经纬志
首页 / OpenLayers / 教程

自定义控件

先看结论

控件是地图上的一块界面,交互是对鼠标手势的响应。 大部分「按钮点了没反应」的问题,都是把这两者的职责搞混了。

最后验证 2026-11-03 · OpenLayers 10.3.1 · 难度:进阶 · 核验 2026-10-29 · 引用本文
维度ol.control.Controlol.interaction.Interaction
职责界面元素,按钮、面板、比例尺手势与鼠标事件,绘制、拖动、选择
挂载map.addControl()map.addInteraction()
位置地图容器或指定 DOM 节点无界面
典型用途底图切换、图层开关、状态显示绘制、修改、框选、测距

一个控件可以调用交互对象的方法,比如按钮触发 draw.interaction 的启用。 反过来不建议:交互里直接改 DOM 会让状态难以追踪。

最小可用的控件

继承 ol.control.Control,在构造里准备好 DOM,交给父类。 父类的构造参数 element 就是最终显示的内容。

class BaseSwitcher extends ol.control.Control {
  constructor(opt_options) {
    const options = opt_options || {};

    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'base-switcher-btn';
    button.textContent = '切换底图';
    button.title = '在影像与街道底图之间切换';

    const element = document.createElement('div');
    element.className = 'base-switcher ol-unselectable ol-control';
    element.appendChild(button);

    super({ element: element, target: options.target });

    this.onToggle_ = options.onToggle;
    button.addEventListener('click', this.handleClick_.bind(this), false);
  }

  handleClick_(event) {
    event.preventDefault();
    if (this.onToggle_) this.onToggle_();
  }
}

var switcher = new BaseSwitcher({
  onToggle: function () {
    var usingImagery = map.getLayers().item(0).get('name') === 'imagery';
    map.getLayers().setAt(0, usingImagery ? streetLayer : imageryLayer);
  }
});

map.addControl(switcher);

类名里的 ol-unselectable 阻止地图的拖动与缩放手势穿透到控件上, ol-control 提供默认的定位与背景样式。缺了前者, 点按钮时地图会跟着平移。

不要用箭头函数做事件回调。控件销毁时无法解绑匿名函数。 用 bind(this) 保存引用,并在 setMap(null) 时移除监听。

控件与地图状态同步

比例尺、坐标读数这类控件需要跟随视图变化。重写 setMap, 在拿到地图实例后注册监听,移除时解绑。

class ResolutionReadout extends ol.control.Control {
  constructor() {
    const element = document.createElement('div');
    element.className = 'resolution-readout ol-unselectable';
    super({ element: element });

    this.element_ = element;
    this.moveKey_ = null;
  }

  setMap(map) {
    if (this.moveKey_) {
      ol.Observable.unByKey(this.moveKey_);
      this.moveKey_ = null;
    }

    super.setMap(map);
    if (!map) return;

    this.moveKey_ = map.on('moveend', this.update_.bind(this));
    this.update_();
  }

  update_() {
    const map = this.getMap();
    if (!map) return;

    const view = map.getView();
    const mpp = ol.proj.getPointResolution(
      view.getProjection(), view.getResolution(), view.getCenter()
    );

    this.element_.textContent = '1 像素 ≈ ' + mpp.toFixed(1) + ' 米';
  }
}

map.addControl(new ResolutionReadout());

getResolution() 的单位是地图单位/像素。在 Web 墨卡托下, 高纬度地区的实际地面距离会被放大,因此要用 getPointResolution 换算,不能直接用分辨率乘以比例尺。

把控件放到页面工具栏

控件不一定非要在图面上。传入 target 或直接把 getElement() 挂到页面节点,就能放进顶栏、侧边面板。 此时地图交互事件不会干扰这些元素。

var toolbar = document.getElementById('map-toolbar');

var switcher = new BaseSwitcher({ onToggle: toggleBase });
map.addControl(switcher);

// 挂到页面工具栏,而不是地图容器
toolbar.appendChild(switcher.getElement());

// 需要移除时两者都要处理
map.removeControl(switcher);
switcher.getElement().remove();

注意事项

  • setMap 可能被调用多次(重新挂载),注册监听前先解绑旧的。
  • 控件元素的样式要写在自己的类名下,直接改 .ol-control 会影响所有内置控件。
  • 按钮的 click 事件要 preventDefault(),否则表单场景会触发表单提交。
  • 控件里读地图状态必须判空:getMap() 在控件被移除后返回 undefined。
  • 大量自定义控件会遮挡地图,折叠成菜单比平铺一排更实用。
引用本文

经纬志(2026)。《自定义控件》。https://www.jingweizhi.com/openlayers/tutorial/custom-control.html(访问日期 2026-11-03)。