开源纯C#工控网关+组态软件(九)定制Visual Studio
一、???引子
因?yàn)樽罱苊?#xff08;lan),很久沒(méi)發(fā)博了。不少朋友對(duì)那個(gè)右鍵彈出菜單和連線的功能很感興趣,因?yàn)閂S本身是不包含這種功能的。
?
?大家想這是什么鬼,怎么我的設(shè)計(jì)器沒(méi)有,其實(shí)這是一個(gè)微軟黑科技,如果用好,VS可以打造為你專用的神兵利器。
為什么我要擴(kuò)展Visual Studio的界面設(shè)計(jì)器?當(dāng)時(shí)我在設(shè)計(jì)組態(tài)軟件的時(shí)候面臨最大的困難大概就是設(shè)計(jì)器了。一套成熟的組態(tài)設(shè)計(jì)器包括:界面設(shè)計(jì)器(包括工具欄、設(shè)計(jì)器、屬性管理器)、腳本編輯器(各種語(yǔ)法高亮、語(yǔ)法檢查、自動(dòng)完成等等等等)、編譯(解釋)器、調(diào)試器、解決方案管理器(如何組織項(xiàng)目、導(dǎo)入/導(dǎo)出文件、添加資源、添加引用等等等等),說(shuō)出來(lái)嚇?biāo)廊?#xff0c;這些功能絕對(duì)不是我這類單兵作戰(zhàn)人員能搞定的。那是微軟、西門(mén)子這種級(jí)別的巨型公司以按人年計(jì)算的成本完成的。也曾經(jīng)想過(guò)套用網(wǎng)上開(kāi)源設(shè)計(jì)器,搜了半天,得出一個(gè)結(jié)論:網(wǎng)上的都是一些簡(jiǎn)單的DEMO或者原型設(shè)計(jì),和我想實(shí)現(xiàn)的目標(biāo)還差的太遠(yuǎn),完善的好東西一般是不會(huì)開(kāi)源的。
但是仔細(xì)想一下我上面列舉的功能,不就是Visual Studio現(xiàn)成的功能嗎?放著這個(gè)宇宙第一IDE不用,想自己重新造輪子,估計(jì)寫(xiě)到老都沒(méi)有什么結(jié)果。于是我想能不能通過(guò)擴(kuò)展VS,去實(shí)現(xiàn)一些組態(tài)軟件的特殊要求功能,比如常用的變量組態(tài)編輯器、連線這類的功能?萬(wàn)能的谷歌讓我找到了我想要的技術(shù): WPF(含Blend) 設(shè)計(jì)器擴(kuò)展。
二、???什么是WPF設(shè)計(jì)器擴(kuò)展
WPF設(shè)計(jì)器,常規(guī)的界面就是 工具欄+XAML編輯器+界面設(shè)計(jì)器。界面設(shè)計(jì)器包括右鍵編輯菜單、設(shè)計(jì)器裝飾(如錨點(diǎn)進(jìn)行縮放、旋轉(zhuǎn)),屬性編輯器等。這些功能已經(jīng)很強(qiáng)大,完善了;但考慮到用戶的特殊需求,VS提供??? 了強(qiáng)大的擴(kuò)展功能,參考https://msdn.microsoft.com/zh-cn/library/windows/desktop/bb675306(v=vs.90).aspx?的介紹:
WPF 設(shè)計(jì)器基于一個(gè)具有可擴(kuò)展的體系結(jié)構(gòu)的框架,用戶可以擴(kuò)展這種框架以創(chuàng)建自己的自定義設(shè)計(jì)體驗(yàn)。
通過(guò)擴(kuò)展 WPF 設(shè)計(jì)器對(duì)象模型,可以在很大程度上自定義 WPF 內(nèi)容的設(shè)計(jì)時(shí)外觀和行為。例如,可以通過(guò)下列方式擴(kuò)展 WPF 設(shè)計(jì)器:
利用增強(qiáng)的圖形自定義移動(dòng)并調(diào)整標(biāo)志符號(hào)的大小。
向設(shè)計(jì)圖面添加一個(gè)標(biāo)志符號(hào),在鼠標(biāo)移動(dòng)時(shí)該標(biāo)志符號(hào)可以使所選控件傾斜。
在不同工具之間修改控件的設(shè)計(jì)時(shí)外觀和行為。
WPF 設(shè)計(jì)器 體系結(jié)構(gòu)支持 WPF 的所有表現(xiàn)力。這樣便可以創(chuàng)建很多以前不可能擁有的可視化設(shè)計(jì)體驗(yàn)。
也就是說(shuō),WPF設(shè)計(jì)器擴(kuò)展提供了一套API,可以自定義裝飾器(如點(diǎn)選控件出現(xiàn)的旋轉(zhuǎn)、拖放、拉伸、定位錨點(diǎn))、右鍵菜單(如編輯、排序、對(duì)齊、剪切)、屬性編輯器,并控制它們的行為;甚至可以改變?cè)O(shè)計(jì)器的外觀。是不是很強(qiáng)大?然而這一黑科技很少人知道,而且為了實(shí)現(xiàn)設(shè)計(jì)器擴(kuò)展,你必須嚴(yán)格遵守一些特殊的規(guī)則,而且設(shè)計(jì)器擴(kuò)展的調(diào)試方式也很特殊。同時(shí),在WPF設(shè)計(jì)器的擴(kuò)展基本可以不修改就移植到Blend。
三、???如何實(shí)現(xiàn)設(shè)計(jì)器擴(kuò)展
API總體架構(gòu)
?
VS的狀態(tài)分為設(shè)計(jì)時(shí)和運(yùn)行時(shí)。設(shè)計(jì)時(shí)就是你打開(kāi)VS,拖拽控件,界面布局,屬性設(shè)置,代碼編寫(xiě),打交道的對(duì)象是Visual Studio;運(yùn)行時(shí)就是你編譯運(yùn)行自己的exe文件。
WPF的界面設(shè)計(jì)器,其核心目標(biāo)就是對(duì)控件(Contorl)的控制,包括對(duì)控件的拖放、旋轉(zhuǎn)、移動(dòng)、屬性編輯等。而在設(shè)計(jì)時(shí)如果要操作控件,首先要在設(shè)計(jì)、編輯過(guò)程中通過(guò)一些API“發(fā)現(xiàn)”要操作的控件,并使其能與VS設(shè)計(jì)器互動(dòng)。API這里使用了一個(gè) “提供者模式”來(lái)實(shí)現(xiàn):對(duì)裝飾器、菜單、屬性編輯器等的操作功能,提供了相應(yīng)的Provider來(lái)實(shí)現(xiàn),如裝飾器的AdornerProvider,右鍵菜單的ContextMenuProvider?等。所有的Provider都遵循這樣的場(chǎng)景:當(dāng)你做了一個(gè)“選擇”的動(dòng)作(比如拖動(dòng)一個(gè)控件旋轉(zhuǎn)-對(duì)應(yīng)AdornerProvider的Active事件;或點(diǎn)了某個(gè)右鍵菜單-對(duì)應(yīng)ContextMenuProvider的Execute事件),進(jìn)而通過(guò)動(dòng)作事件的PrimarySelection參數(shù)獲取相對(duì)應(yīng)的ModelItem-控件在設(shè)計(jì)時(shí)的“馬甲”,進(jìn)而通過(guò)ModelItem的GetCurrentValue方法找到你選擇的對(duì)象。大家也許會(huì)問(wèn),設(shè)計(jì)器擴(kuò)展為何要多此一舉的對(duì)控件加一層外殼ModelItem,直接操作控件不就行了嗎?回答是,你對(duì)控件的設(shè)計(jì)時(shí)操作,例如對(duì)控件的激活,使之成為設(shè)計(jì)器選中的控件,這一行為在控件本身并沒(méi)有定義;而設(shè)計(jì)器也要通過(guò)自己“理解”的上下文才能與控件交互。ModelItem將用戶對(duì)控件的操作反饋給設(shè)計(jì)器,或者將設(shè)計(jì)的動(dòng)作告知用戶,起了關(guān)鍵的中介作用。而設(shè)計(jì)器本身的“馬甲”是DesignerView,可以通過(guò)這個(gè)類獲取設(shè)計(jì)器當(dāng)前設(shè)置,如當(dāng)前界面大小、縮放比例等。
如何實(shí)現(xiàn)
要實(shí)現(xiàn)一個(gè)完整的設(shè)計(jì)器擴(kuò)展,要經(jīng)歷以下過(guò)程:
定義元數(shù)據(jù),設(shè)計(jì)器需要知道哪些控件具有哪些擴(kuò)展。這是通過(guò)Metadata?類來(lái)實(shí)現(xiàn)的:Metadata?類有一個(gè)AttributeTable方法,在其中構(gòu)建了控件和功能(即相應(yīng)的Provider)的映射關(guān)系。
using Microsoft.Windows.Design.Features;
using Microsoft.Windows.Design.Metadata;
?
?
[assembly: ProvideMetadata(typeof(HMIControl.VisualStudio.Design.Metadata))]
namespace HMIControl.VisualStudio.Design
{
? ? internal class Metadata : IProvideAttributeTable
? ? {
? ? ? ? // Accessed by the designer to register any design-time metadata.
? ? ? ? public AttributeTable AttributeTable
? ? ? ? {
? ? ? ? ? ? get
? ? ? ? ? ? {
? ? ? ? ? ? ? ? AttributeTableBuilder builder = new AttributeTableBuilder();
? ? ? ? ? ? ? ? //InitializeAttributes(builder);
? ? ? ? ? ? ? ? // Add the adorner provider to the design-time metadata.
? ? ? ? ? ? ? ? builder.AddCustomAttributes(
? ? ? ? ? ? ? ? ? ? typeof(LinkableControl),
? ? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(ControlAdornerProvider))
? ? ? ? ? ? ? ? ? ? //new FeatureAttribute(typeof(TagComplexContextMenuProvider))
? ? ? ? ? ? ? ? ? ? );
? ? ? ? ? ? ? ? builder.AddCustomAttributes(
? ? ? ? ? ? ? ? ? typeof(HMIControlBase),
? ? ? ? ? ? ? ? ? //new FeatureAttribute(typeof(LinkLineAdornerProvider)),
? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(TagComplexContextMenuProvider)));
? ? ? ? ? ? ? ? builder.AddCustomAttributes(
? ? ? ? ? ? ? ? ? ? typeof(LinkLine),
? ? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(LinkLineAdornerProvider)),
? ? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(TagComplexContextMenuProvider)));
? ? ? ? ? ? ? ? builder.AddCustomAttributes(
? ? ? ? ? ? ? ? ? ? typeof(ButtonBase),
? ? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(TagWriterContextMenuProvider)));
? ? ? ? ? ? ? ? builder.AddCustomAttributes(
? ? ? ? ? ? ? ? ? typeof(HMIButton),
? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(TagWindowContextMenuProvider)),
? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(TagComplexContextMenuProvider)),
? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(TagWriterContextMenuProvider)));
? ? ? ? ? ? ? ? builder.AddCustomAttributes(
? ? ? ? ? ? ? ? ? typeof(FromTo),
? ? ? ? ? ? ? ? ? new FeatureAttribute(typeof(TagWindowContextMenuProvider)));
? ? ? ? ? ? ? ? return builder.CreateTable();
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
定義具體的Provider,所有的Provider都執(zhí)行如下次序:根據(jù)用戶選擇,找到相關(guān)控件,并進(jìn)行操作,將操作結(jié)果反饋給設(shè)計(jì)器。
根據(jù)設(shè)計(jì)器擴(kuò)展的默認(rèn)規(guī)則,在正確的位置使用正確的命名方式,否則你的擴(kuò)展不會(huì)出現(xiàn)在設(shè)計(jì)器。這些默認(rèn)規(guī)則包括:
命名空間規(guī)則:將設(shè)計(jì)器擴(kuò)展項(xiàng)目的命名空間設(shè)置為HMIControl.VisualStudio.Design(HMIControl即控件庫(kù)的命名空間),以便設(shè)計(jì)器能夠發(fā)現(xiàn)元數(shù)據(jù)。
項(xiàng)目路徑規(guī)則:將項(xiàng)目的輸出路徑設(shè)置為“..\HMIControl\bin\”(HMIControl即控件庫(kù)的項(xiàng)目路徑)。?使控件的程序集與元數(shù)據(jù)程序集位于同一文件夾中,從而可為設(shè)計(jì)器啟用元數(shù)據(jù)發(fā)現(xiàn)。
如何調(diào)試
一段不能加斷點(diǎn)調(diào)試的代碼會(huì)給編寫(xiě)者帶來(lái)很大困擾。但設(shè)計(jì)器擴(kuò)展有一個(gè)特殊性:沒(méi)法在運(yùn)行時(shí)加斷點(diǎn)。好在微軟早就為我們安排好了一切。具體可參考https://msdn.microsoft.com/zh-cn/sqlserver/bb514636
即調(diào)試時(shí)需要更改項(xiàng)目的屬性,設(shè)置啟動(dòng)程序?yàn)閂S的可執(zhí)行文件:?devenv.exe.相當(dāng)于再打開(kāi)一個(gè)新的VS作為運(yùn)行時(shí)。調(diào)試時(shí)打開(kāi)你的設(shè)計(jì)器操作,會(huì)發(fā)現(xiàn)第一個(gè)打開(kāi)的VS中已經(jīng)命中斷點(diǎn)了。
?
?
四、???組態(tài)定制需求的實(shí)現(xiàn)
根據(jù)組態(tài)軟件的特殊需求,有兩個(gè)重要功能是通過(guò)WPF設(shè)計(jì)器擴(kuò)展實(shí)現(xiàn)的:控件連線和右鍵彈出表達(dá)式編輯器,具體代碼在LinkableControlDesign項(xiàng)目中。
界面連線的實(shí)現(xiàn)
設(shè)計(jì)目標(biāo):實(shí)現(xiàn)兩個(gè)HMI控件的連線。每個(gè)控件最多有上下左右四個(gè)位置(即錨點(diǎn),也可以少于四個(gè)甚至沒(méi)有),連線從A控件任一位置引出,自動(dòng)尋找路徑,連到B控件的任一位置;路徑不能穿越其他控件,而應(yīng)自動(dòng)繞開(kāi)。連線均為直線,不能為圓弧線或斜線;在控件位置改變時(shí),連線重新計(jì)算并繪制。
設(shè)計(jì)過(guò)程:具有錨點(diǎn)的控件均繼承LinkableControl類。錨點(diǎn)裝飾器類為ControlAdorner,是一個(gè)控件容器,包含上下左右四個(gè)錨點(diǎn),每個(gè)錨點(diǎn)由PinAdorner 定義,包含錨點(diǎn)的外形、自動(dòng)生成路徑等功能。路徑發(fā)現(xiàn)由PathFinder類實(shí)現(xiàn)。與設(shè)計(jì)器交互通過(guò)繼承AdornerProvider?類實(shí)現(xiàn)。
運(yùn)行過(guò)程:通過(guò)AdornerProvider?類的Activate事件,獲取當(dāng)前點(diǎn)擊(激活)的控件并轉(zhuǎn)換為L(zhǎng)inkableControl,并找到控件的父容器Panel、控件的裝飾器ControlAdorner及其包含的每個(gè)PinAdorner、設(shè)計(jì)器包裝DesignerView。在每個(gè)PinAdorner的鼠標(biāo)點(diǎn)擊和拖放事件內(nèi),可探索到其他控件的錨點(diǎn)、規(guī)劃路徑、生成連線LinkLine。
同時(shí)要考慮設(shè)計(jì)器進(jìn)行縮放時(shí)路徑的變化,在DesignerView的ZoomLevelChanged事件中處理。
右鍵菜單的實(shí)現(xiàn)
設(shè)計(jì)目標(biāo):組態(tài)軟件一般都有自己的變量表達(dá)式編輯器,用來(lái)實(shí)現(xiàn)對(duì)界面控件的動(dòng)畫(huà)效果。如果要求設(shè)計(jì)者手工輸入表達(dá)式,容易出錯(cuò),也沒(méi)有語(yǔ)法檢查,很麻煩。但VS并沒(méi)有提供這個(gè)功能,因此我想到了點(diǎn)選控件,彈出的右鍵菜單加上一個(gè)編輯項(xiàng)。這就要用到ContextMenuProvider的功能。
設(shè)計(jì)過(guò)程:TagComplexContextMenuProvider?繼承了ContextMenuProvider,如果菜單“ComplexEditor”被激活,觸發(fā)Exeute事件,則彈出窗體TagComplexEditor,以設(shè)置控件的動(dòng)畫(huà)關(guān)聯(lián)的變量表達(dá)式;操作結(jié)果將寫(xiě)回控件的TagReadText?屬性。
未來(lái)改進(jìn)
編輯器改進(jìn):支持命令自動(dòng)完成、語(yǔ)法高亮、更完善的語(yǔ)法檢查。。
快捷鍵編輯:目前的右鍵彈出編輯器菜單方式操作還可以進(jìn)一步改進(jìn)為快捷鍵方式。但似乎WPF擴(kuò)展沒(méi)有提供快捷鍵彈出的API,期待進(jìn)一步完善。
相關(guān)文章:?
.NET十年回顧
開(kāi)源純C#工控網(wǎng)關(guān)+組態(tài)軟件
開(kāi)源純C#工控網(wǎng)關(guān)+組態(tài)軟件(三)加入一個(gè)新驅(qū)動(dòng):西門(mén)子S7
開(kāi)源純C#工控網(wǎng)關(guān)+組態(tài)軟件(四)上下位機(jī)通訊原理
開(kāi)源純C#工控網(wǎng)關(guān)+組態(tài)軟件(五)從網(wǎng)關(guān)到人機(jī)界面
開(kāi)源純C#工控網(wǎng)關(guān)+組態(tài)軟件(六)圖元組件
開(kāi)源純C#工控網(wǎng)關(guān)+組態(tài)軟件(七)數(shù)據(jù)采集與歸檔
開(kāi)源純C#工控網(wǎng)關(guān)+組態(tài)軟件(八)表達(dá)式編譯器
github地址:https://github.com/GavinYellow/SharpSCADA。QQ群:102486275
原文地址:http://www.cnblogs.com/evilcat/p/8859223.html
.NET社區(qū)新聞,深度好文,歡迎訪問(wèn)公眾號(hào)文章匯總 http://www.csharpkit.com 
總結(jié)
以上是生活随笔為你收集整理的开源纯C#工控网关+组态软件(九)定制Visual Studio的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
                            
                        - 上一篇: 把旧系统迁移到.Net Core 2.0
 - 下一篇: 使用C#实现适配器模式 (Adapter