1.典型的插件式架構
1.1.什麼是插件式架構
插件式架構設計中主要包括三個重要部分,宿主、插件協議以及插件實現。宿主是指使用插件的部分,該模塊可以是一個類,也可以是多個接口和類組成的模塊。插件協議是指宿主與插件之間的協議,宿主根據這個協議去調用插件的功能,插件根據這個協議去實現宿主需要的功能。插件實現就是基於插件協議實現的一個個具體插件。插件協議一般用接口體現。
1.2.AO中的Command-Tool插件架構
要梳理AO中的Command-Tool插件架構,就要找到對應的宿主、插件協議以及具體的插件實現。
我們先通過ICommand的定義,找下宿主是什麼。
當命令被創建的時候,會傳一個名爲hook的參數進來,該參數爲object類型,名稱翻譯過來叫鉤子。這個名稱挺形象的,相當於這個小小的插件鉤住了一個很龐大宿主對象,這樣在這個插件中,就可以使用宿主對象的任何資源了。
其關係如下圖所示。
我們開發的時候,常用的宿主就是IMapControl、IPageLayoutControl,分別可以有AxMapControl.Object和AxPageLayoutControl.Object獲取。AO API已經爲我們實現了上百個命令或者工具。例如我們最常用的地圖放大工具、地圖全圖命令等。這些工具和命令我們都可以直接使用。
下面代碼展示瞭如何設置當前的地圖工具爲放大工具。
ESRI.ArcGIS.SystemUI.ITool myZoomInTool=new ControlsMapZoomInToolClass(); (myZoomInTool as ICommand).OnCreate(myAxMapControl.Object); (myAxMapControl.Object as IMapControl2). CurrentTool = myZoomInTool;
調用AO API自帶的全圖工具的代碼如下。
ESRI.ArcGIS.SystemUI.ICommand myFullCommand=new ControlsMapFullExtentCommandClass (); myFullCommand.OnCreate(myAxMapControl.Object); myFullCommand. OnClick();
2.ICommnad接口
ICommand接口是插件協議之一,繼承該接口的類都可以成爲命令。即點擊一下執行,不主動與宿主發生鼠標和鍵盤交互。該接口包含的重要成員如下表所示。
序號 |
名稱 |
類型 |
描述 |
1 |
Bitmap |
Int |
命令上顯示的圖標 |
2 |
Caption |
String |
命令上顯示的文字 |
3 |
Checked |
Bool |
命令是否處於選中狀態 |
4 |
Enabled |
Bool |
命令當前是否可用 |
5 |
OnClick |
函數 |
點擊命令時,觸發執行的函數 |
6 |
OnCreate |
函數 |
創建該命令時,調用的函數 |
7 |
Tooltip |
String |
鼠標放到命令上要顯示的文字 |
這些屬性和函數的名字都比較容易理解,一看就知道其作用。如果我們自己繼承ICommand實現一個彈出當前地圖包含幾個圖層的命令,命名爲LayerCountCommand。那麼只要在OnClick函數中,獲取OnCreate函數傳進來的宿主對象,從宿主對象中獲取當前加載的地圖,進而獲取其包含的幾個圖層,使用消息對話框彈出即可。
我們實現命令時候,只需要關注宿主對象就可以了,和其他Command不會發生直接關聯。如果需要和其他Command關聯,那麼這些關聯都通過宿主進行。例如我們剛纔實現的LayerCountCommand在地圖圖層爲0的時候,其處於不可用的狀態,也就是 Enabled屬性等於False。按照正常思維的話,我們會在OpenMapCommand裏面,判斷一下當前圖層個數是否爲0,從而設置設置LayerCountCommand實例的Enabled屬性。那RemoveLayerCommand、AddLayerCommand、NewMapCommand等怎麼辦呢?難道每個命令裏面都要判斷LayerCountCommand是都可用?
當然不會是這樣,我們會爲宿主添加MapChanged和LayersChanged事件,無論用OpenMapCommand還是其他地方,在系統中打開一個新地圖後,宿主對象就會觸發MapChanged事件。而我們實現的LayerCountCommand會監測該事件,當該事件觸發後,LayerCountCommand會判斷當前地圖有幾個圖層,從而設置自己是否是可用。
這樣LayerCountCommand只關係宿主的信息,間接的和OpenMapCommand產生了關聯。而LayerCountCommand自己可以決定自己是否可用,這就形成了高內聚,低耦合的設計。
3.ITool接口
ITool接口也是插件協議之一,實現該接口的類我們成爲工具,這些工具是可以和地圖顯示控件進行鼠標、鍵盤交互的。該接口包含的重要成員如下表所示。
序號 |
名稱 |
類型 |
描述 |
1 |
Cursor |
Int |
鼠標在地圖顯示控件上的樣式 |
2 |
Deactivate |
函數 |
工具失活的時候觸發的函數 |
3 |
OnMouseDown |
函數 |
鼠標按下執行的函數 |
4 |
OnMouseMove |
函數 |
鼠標移動執行的函數 |
5 |
OnMouseUp |
函數 |
鼠標談起執行的函數 |
6 |
OnDblClick |
函數 |
鼠標雙擊地圖顯示控件執行的函數 |
7 |
OnKeyDown |
函數 |
鍵盤按鍵按下執行的函數 |
8 |
OnKeyUp |
函數 |
鍵盤按鍵彈起執行的函數 |
每個宿主對象都有CurrentTool屬性,如果點擊MapZoomInTool工具,那麼該宿主的CurrentTool就是該工具了。當鼠標在宿主對象上按下的時候,會自動調用宿主對象CurrentTool屬性值的OnMouseDown,也就是MapZoomInTool工具的OnMouseDown函數。當鼠標在宿主對象上移動的時候,會自動調用MapZoomInTool的OnMouseMove函數。其他動作一次類推,並且宿主對象讀取CurrentTool的Cursor作爲當前的鼠標樣式。
要做一個放大工具,需要考慮拉框放大、點擊放大等。但有了宿主程序,就可以把這些代碼完整的封裝到MapZoomInTool類中。而實現地圖縮小功能MapZoomOutTool以及地圖平移的MapPanTool,完全不需要知道其他工具的存在,也不關心其他Tool都做了什麼。
在ArcMap中,地圖放大、地圖縮小、地圖平移、地圖全圖地圖即放大、地方即縮小、上一視圖和下一視圖這幾個Tool和Command關係非常密切。如下圖所示。
但通過這種插件式架構,他們不光代碼之間無直接聯繫,而且還可以做到隨意組合使用。例如我們開發的系統可以使用放大、縮小和平移三個工具,也可以加上全圖,或者再加上其他工具,都可以正常運行。這種模式非常值得我們思考和借鑑。
4.我們能借鑑到什麼
ArcMap就是通過這種插件式架構把各種Command和Tool組合起來的,而這些Comamnd和Tool的背後是一個個功能點。不光使得ArcMap可以持續集成那麼多功能,甚至可以開放接口,讓開發人員直接在ArcMap上擴展。
那這種模式是不是可以借鑑到我們的軟件開發中?答案是肯定的,下面我們就來設計自己的App-Command框架。
5.我們自己的App-Command框架
5.1.爲什麼再設計一套App-Command框架
爲什麼我們要自己再設計一套App-Command框架,而不直接使用AO API中的AxControl-ICommand這套已經非常好的框架呢?
1、宿主不同。我們系統的宿主對象除了可能要包含MapControl等地圖顯示控件外,還可能會包含我們業務系統特有的信息。例如當前登錄用戶,在一些Command中,可能需要根據當前登錄用戶的覺得來判斷功能是否可用等。
2、AO中的ICmmand和ITool已經和UI綁定到一起了,我們並不想直接用AO中定義的ToolBar,這樣會和我們的系統風格不一致。還有ICommand中定義的Bitmap以及ITool中定義的Cursor都是int類型,這並不符合我們的使用習慣。如果我們使用傳統菜單+工具條的模式,使用的都是16*16的圖標,如果我們採用Office的Ribbon風格,那麼可能會出現很多32*32的圖標,這個如何兼容?
3、我們想讓我們定義的工具適應更多的UI。例如定義的Command和Tool和綁定到WPF自帶的按鈕上,也可以綁定到第三方庫例如DEV定義的按鈕上。這就需要多UI進行抽象。
5.2.基於AO設計的App-Command框架
我們參考借鑑AO,定義我們自己的App-Command框架如下,定義的時候,主要是解決上述的幾個問題。我們定義的框架如下。
IApplication、ICommand、IMapTool以及ICmmandUI四個接口以及MapApplication類是整個框架的核心部分。除了上圖中體現出來的內容外,框架還包含Command、MapTool以及 ViewSynchronizer等基類和輔助類。
5.3.ITool接口是怎麼交互的
我們在MapApplication類中封裝了宿主與ITool接口的交互。封裝代碼如下。
this.AxMapControl.OnMouseDown += (x, y) => { this._CrruteTool.OnMouseDown(y.button, y.shift, y.x, y.y); }; this.AxMapControl.OnMouseMove += (x, y) => { this._CrruteTool.OnMouseMove(y.button, y.shift, y.x, y.y); }; this.AxMapControl.OnMouseUp += (x, y) => { this._CrruteTool.OnMouseUp(y.button, y.shift, y.x, y.y); }; this.AxMapControl.OnDoubleClick += (x, y) => { this._CrruteTool.OnDblClick(); }; this.AxMapControl.OnKeyDown += (x, y) => { this._CrruteTool.OnKeyDown(y.keyCode, y.shift); }; this.AxMapControl.OnKeyUp += (x, y) => { this._CrruteTool.OnKeyUp(y.keyCode, y.shift); };
對於宿主來說,並不關心當前使用的到底是哪個Tool,只管在觸發動作的時候,去調用當前工具對應的函數即可。
工具切換的代碼如下。
public IMapTool CrruteTool { get { return this._CrruteTool; } set { this._CrruteTool.OnDeActivate(); this._CrruteTool.IsChecked = false; this._CrruteTool = value; if (this._CrruteTool == null) { this._CrruteTool = new NullMapTool(this); } this._CrruteTool.OnActive(); this._CrruteTool.IsChecked = true; } }
切換工具的時候,首先要調用工具的OnDeActivate函數,把當前工具的使用痕跡清理掉。設置新工具,調用新工具的OnActive函數,激活該工具。
5.4.封裝AO已有的Command
AO本身爲我們提供了很多已經實現好的工具和命令,如何把這些命令融合到我們自己的框架中呢?
Command以全圖命令爲例。
public class MapFullExtentCommand : MapCommand { private ESRI.ArcGIS.SystemUI.ICommand _EsriCommand = null; public MapFullExtentCommand(MapApplication pMapApplication) : base(pMapApplication) { this._EsriCommand = new ControlsMapFullExtentCommandClass(); this._EsriCommand.OnCreate(pMapApplication.MapControl); this.SetIcon(CommandIconSize.IconSize16, "MapTools/Res/MapFullExtent16.png"); } public override void OnClick() { base.OnClick(); this._EsriCommand.OnClick(); } }
我們初始化了一個AO定義的ControlsMapFullExtentCommandClass類,並與我們定義的MapApplication中的MapControl綁定。實現命令點擊函數的時候,直接調用AO中定義的全圖類的OnClick函數即可。
Tool以地圖放大工具爲例。
public class MapZoomInTool : MapTool { private readonly ESRI.ArcGIS.SystemUI.ITool _EsriTool = null; public MapZoomInTool(MapApplication pMapApplication) : base(pMapApplication) { this._EsriTool = new ControlsMapZoomInToolClass(); this.SetIcon(CommandIconSize.IconSize16, "MapTools/Res/MapZoomIn16.png"); } public override void OnActive() { base.OnActive(); (this._EsriTool as ESRI.ArcGIS.SystemUI.ICommand).OnCreate(this.MapApplication.ActiveControl); this.MapApplication.ActiveControl.CurrentTool = this._EsriTool; } public override void OnDeActivate() { base.OnDeActivate(); this.MapApplication.ActiveControl.CurrentTool = null; } }
我們初始化了一個AO定義的ControlsMapZoomInToolClass類,在激活該工具的時候和當前激活的Control綁定,如果是數據模式,會綁定MapControl,如果是佈局模式,會綁定PageLayoutControl。並把定義的工具賦值給當前激活的Control的CurrentTool屬性。失活的時候,把當前激活Control的CurrentTool設置爲null。
這樣我們就可以充分利用AO已經實現的各類命令和工具了。
5.5.擴展AO已有的Command
如果我們想在已有工具的基礎上做些其他事情呢?例如在出圖的時候,選擇一個元素,在右側顯示該元素的屬性面板。正常思路下,我們會點擊PageLayoutControl,根據座標去判斷是否選中的Element,如果選中了,則把Element顯示爲選中狀態,並獲取該對象,在右側顯示其屬性面板。
那是不是有更簡單的方法?AO是有選擇Element工具的,類名稱爲ControlsSelectToolClass,使用該工具可以使用鼠標進行點選、框選、刪除、移動以及調整元素大小等操作,這些功能如何我們自己去寫代碼實現,將有非常大的工作量。如果能用這個工具,那就再好不過了。但我們需要解決兩個問題。
1、AO中定義的選擇類,在選擇元素後,我們要捕捉到該動作,並獲取選中的元素,顯示元素的面板;
2、AO中定義的選擇類,選擇元素後,按下Delete鍵,會刪除元素,這個邏輯我們需要控制,禁止刪除MapFarme,並且刪除其他元素的時候,要彈出提示是否確定刪除對話框,確定後,再刪除。
代碼定義如下。
public class SelectTool : MapTool { private readonly LayoutDesignApplication _LayoutDesignAplication = null; private readonly ControlsSelectToolClass _EsriTool = null; public SelectTool(LayoutDesignApplication pLayoutDesignAplication) : base(pLayoutDesignAplication) { this._LayoutDesignAplication = pLayoutDesignAplication; this._EsriTool = new ControlsSelectToolClass(); this.Caption = "Select"; this.Tooltip = "Select"; this.SetIcon(CommandIconSize.IconSize16, "Designs/Res/Select16.png"); } public override void OnActive() { base.OnActive(); this._EsriTool.OnCreate(this._LayoutDesignAplication.ActiveControl); } public override void OnKeyDown(int keyCode, int shift) { if (keyCode == (int)ConsoleKey.Delete) { IGraphicsContainerSelect myGraphicsContainerSelect = this._LayoutDesignAplication.PageLayoutControl.GraphicsContainer as IGraphicsContainerSelect; IElement mySelectElement = myGraphicsContainerSelect.DominantElement; if (mySelectElement is IMapFrame == true) { return; } MessageBoxResult myMessageBoxResult = MessageBox.Show("Is it determined to remove?", "Info", MessageBoxButton.YesNo); if (myMessageBoxResult != MessageBoxResult.Yes) { return; } base.OnKeyDown(keyCode, shift); this._EsriTool.OnKeyDown(keyCode, shift); } else { base.OnKeyDown(keyCode, shift); } } public override void OnMouseDown(int button, int shift, int x, int y) { //如果是中鍵 if (button == 4) { this._LayoutDesignAplication.AxControlPan(); } else { this._EsriTool.OnMouseDown(button, shift, x, y); } } public override void OnMouseUp(int button, int shift, int x, int y) { base.OnMouseUp(button, shift, x, y); this._EsriTool.OnMouseUp(button, shift, x, y); IPageLayoutControl myPageLayoutControl = this._LayoutDesignAplication.PageLayoutControl; IPageLayout myPageLayout = myPageLayoutControl.PageLayout; IGraphicsContainerSelect myGraphicsContainerSelect = myPageLayout as IGraphicsContainerSelect; IElement myDominantElement = myGraphicsContainerSelect.DominantElement; } public override void OnMouseMove(int button, int shift, int x, int y) { base.OnMouseMove(button, shift, x, y); this._EsriTool.OnMouseMove(button, shift, x, y); } }
5.6. 完全自定義Command
完全自定義的Command就比較簡單些了。例如我們定義異常當前選中的圖層命令,定義如下。
public class LayerRemoveCommand : MapCommand { public LayerRemoveCommand(MapApplication pMapApplication) : base(pMapApplication) { this.Caption = "Remove"; this.IsEnabled = false; this.MapApplication.OnActiveStateChanged += (x, y) => { this.UpdateIsEnableState(); }; this.MapApplication.OnSelectTocObjectChanged += (x, y) => { this.UpdateIsEnableState(); }; } public override void OnClick() { base.OnClick(); ILayer myLayer = this.MapApplication.SelectTocObject as ILayer; if (myLayer == null) { MessageBox.Show("Please Select A Layer。"); return; } if (MessageBox.Show("Are You Sure Remove The Layer?", "Info", MessageBoxButton.YesNo) == MessageBoxResult.Yes) { this.MapApplication.MapControl.ActiveView.FocusMap.DeleteLayer(myLayer); this.MapApplication.TOCControl.Update(); } } private void UpdateIsEnableState() { if (this.MapApplication.ActivePattern == MapActivePattern.None) { this.IsEnabled = false; return; } ILayer myLayer = this.MapApplication.SelectTocObject as ILayer; this.IsEnabled = (myLayer != null); } }
該定義就可以添加到圖層的右鍵菜單上,用來移除當前選中的圖層。我們不光可以通過調用宿主的屬性和事件來控制自己是否可用,還可以加入很多邏輯判斷。例如如果沒有選擇任何圖層,則提示用戶請選擇一個圖層。在移除的時候,可以提示用戶是否確定移除等。
自定義的工具如下所示。
public class PointTextTool : MapTool { private LayoutDesignApplication _PLAplication = null; public PointTextTool(LayoutDesignApplication pPLAplication) : base(pPLAplication) { this._PLAplication = pPLAplication; this.Caption = "Insert Text"; this.Tooltip = "Insert Text"; this.SetIcon(CommandIconSize.IconSize16, "Designs/Res/Text16.png"); } public override void OnMouseDown(int button, int shift, int x, int y) { base.OnMouseDown(button, shift, x, y); IPoint myPagePoint = this._PLAplication.PageLayoutControl.ToPagePoint(x, y); IPageLayout myPageLayout = this._PLAplication.PageLayoutControl.PageLayout; IGraphicsContainerSelect myGraphicsContainerSelect = myPageLayout as IGraphicsContainerSelect; myGraphicsContainerSelect.UnselectAllElements(); PointTextItem myPointTextItem = new PointTextItem(); MapFrameItem myMapFrameItem = this._PLAplication.LayoutDesign.MapFrameItem; myPointTextItem.X = myPagePoint.X; myPointTextItem.Y = myPagePoint.Y; this._PLAplication.LayoutDesign.PageLayoutItemList.Add(myPointTextItem); myPointTextItem.Apply(this._PLAplication); (myPageLayout as IActiveView).PartialRefresh(esriViewDrawPhase.esriViewGraphics, null, null); this._PLAplication.CrruteTool = this._PLAplication.SelectTool; } }
實現工具的OnMouseDown函數,獲取當前點擊位置的座標,實例化一個文本元素,添加到該位置。然後馬上把工具切換到系統定義好的SelectTool上,這樣會避免不小心點擊兩次,添加了兩個文本元素,提高用戶體驗。
切換到SelectTool後,再次點擊剛添加文本元素就可以選中該元素,這樣右側該元素的信息面板就展示出來了,完成了一個非常自然的操作過程。
通過命令和工具通過自己控制自己的狀態、行爲等,可以做到很細微的邏輯控制,並且這些操作會很好的封裝在自己的代碼中。這樣系統功能可以通過實現各類Command和Tool不斷擴展系統功能,但又不會影響系統的整體結構。
6.AO中已經實現的Command和Tool
AO中實現了二三百個命令和工具,我們常用的大概有幾十個。可以在幫助中通過查看ICommand接口,查看到底有哪些實現。
也可以通過ArcMap查找,出現在ArcMap工具條上的按鈕,大部分都能在此找到對應的類。