控件
發布日期: 7/19/2004 | 更新日期: 7/19/2004
chris tacke, windows embedded mvp
applied data systems
適用于:
microsoft windows ce .net
smart device extensions for microsoft visual studio .net
摘要:學習如何使用 smart device extensions for microsoft visual studio .net (sde) 創建自定義控件。
本頁內容
簡介
問題
對象模型
構建自定義連接器
簡介
smart device extensions for microsoft visual studio .net (sde) 提供了一種可以在應用程序中使用的很好的基本控件。遺憾的是,嵌入式設備應用程序涉及的范圍非常廣,這就使得開發人員幾乎肯定會在某些地方缺少合適的控件,此時,基本上有兩個選擇:重新進行應用程序的結構設計以使用可用的控件,或者采用您自己的自定義控件。
sde 的第一個版本不支持設計時自定義控件,這意味著為了使用它們,必須手動編寫將它們放入窗體并設置其大小和屬性的代碼。它只需很少的額外工作量,并且只需要您接受沒有可用于自定義控件的 form design support 這一事實。
返回頁首
問題
最近,我一直在為 visual studio .net 創建類庫,用于包裝很多硬件的功能。通過使用一個可以為他們完成所有 p/invoking 和資源管理工作的類庫,托管代碼開發人員使用這個類庫來訪問機載微型控制器和 microsoft windows ce 端口就容易多了。我開發用于 graphics master 設備的 i/o 的類庫,以便提供對兩個單獨的頭上的引腳的讀取和寫入功能。
我需要一個測試和示例應用程序,該程序能夠使用戶輕松地通過恰當的圖形接口設置或讀取數字 i/o 狀態并讀取模擬 i/o。我希望有某個東西看起來像示意圖上的接頭或類似板上的物理插頭。由于我要處理兩個物理上不同大小的接頭,所以我需要多個控件,或最好是一個可以定義大小的控件。很顯然,sde 的工具箱中沒有我想要的控件。
我本來可以使用大量的 label、checkbox、picturebox 和 textbox,但是我認為這種替代方案看起來很難看。讓我們嘗試編寫自己的控件。
返回頁首
對象模型
第一個任務是決定整個對象模型。我們需要什么樣的組成部分,這些組成部分將如何融合在一起,它們如何相互交互,如何與它們的環境交互?
圖 1. 我的連接器控件概念
我們將創建連接器,用來包含大小可變的引腳集合,以便能夠連接不同大小的接頭。每個引腳必須有可以放在被顯示的“引腳”的左側或右側(取決于它是偶數還是奇數引腳)的標識標簽。每個引腳還可以是數字的或模擬的 i/o,因此每個引腳都需要有范圍從零到 0xffff 的單獨的值。最好能夠一眼即可識別每個引腳的類型和值,所以將需要使用一些顏色。當然,并非接頭上的所有引腳都可用于 i/o,所以我們需要能夠禁用它們中的一部分,此外,我們希望引腳是交互的,這樣當我們接通一個引腳時,它可以做某些操作,比如更改狀態。
圖 1 是一個控件在屏幕上顯示的外觀的很好模型。
基于這些要求,我們提出了一個如圖 2 所示的對象模型。
圖 2. 控件對象模型
整體的思路是,我們將有一個 connector 基類,然后從它派生出其他幾個自定義的 connector 類。connector 將包含一個 pins 類,這個類只是通過從 collectionbase 派生,使用索引器來公開 pin 對象的 listarray。
實現 pin 對象
因為此控件的骨干是 pin 對象,所以我們首先介紹它。pin 對象將處理控件的大多數顯示屬性,并處理用戶交互。一旦我們可以成功地在窗體上創建、顯示單個引腳并與之交互,構建一個連接器將它們組合在一起就非常簡單了。
pin 對象有四個在創建它時必須設置的屬性。默認的構造函數會設置它們中的每一個,但其他構造函數還可以用來允許創建者傳遞非默認的值。
最重要的屬性是 alignment。這個屬性確定了繪制對象時文本和引腳的位置,但更重要的是,設置屬性時,它將創建和放置用于繪制引腳和文本的矩形。這些矩形的使用將在隨后解釋 ondraw 時進行討論。
清單 1 顯示了基本構造函數和 alignment 屬性的代碼。為引腳子組件周圍所定義的偏移量和邊框使用了常量,但這些常量也很容易成為控件的其他屬性。
清單 1. 引腳構造函數和 alignment 屬性
public pin()
{
showvalue = false;
pinvalue = 0;
type = pintype.digital;
alignment = pinalignment.pinonright;
}
public pinalignment alignment
{ // determines where the pin rectangle is placed
set
{
align = value;
if(value == pinalignment.pinonright)
{
this.pinborder = new rectangle(
this.clientrectangle.width - (pinsize.width + 10),
1,
pinsize.width + 9,
this.clientrectangle.height - 2);
this.pinbounds = new rectangle(
this.clientrectangle.width - (pinsize.width + 5),
((this.clientrectangle.height -
pinsize.height) / 2) + 1,
pinsize.width,
pinsize.height);
this.textbounds = new rectangle(
5,
5,
this.clientrectangle.width - (pinsize.width + 10),
20);
}
else
{
this.pinborder = new rectangle(
1,
1,
pinsize.width + 9,
this.clientrectangle.height - 2);
this.pinbounds = new rectangle(
6,
this.clientrectangle.height - (pinsize.height + 4),
pinsize.width,
pinsize.height);
this.textbounds = new rectangle(
pinsize.width + 10,
5,
this.clientrectangle.width - (pinsize.width + 10),
20);
}
this.invalidate();
}
get
{
return align;
}
}
由于 pin 對象不會提供很好的用戶交互或可自定義性,所以引腳的核心功能是我們將重寫的繪圖例程 ondraw,重寫該例程是為了可以由我們來繪制整個引腳。
每個引腳將繪制三個部分:引腳本身將是一個圓(除非它是 pin 1,這時它將是一個方塊),我們將圍繞引腳繪制邊框矩形,然后在引腳的左側或右側留出一個區域用來繪制引腳的文本。
要繪制引腳,我們首先確定表示實際引腳的圓所使用的顏色。如果引腳被禁用,它的顏色是灰色。如果啟用,則要確定它是什么類型。模擬引腳將是綠色,而數字引腳根據情況而不同,如果是低 (關)則是藍色,如果是高(開)則是橙色。
下一步,我們使用 fillellipse 來繪制所有實際的引腳,但 pinnumber=1 時除外,這時使用 fillrectangle 繪制引腳。通過繪制在矩形 (pinbounds) 中而不是控件的邊界上,我們能夠在創建引腳時設置引腳的位置(左側或右側),并且從這一點開始,我們可以在不用關心引腳的位置的情況下進行繪制。
下一步我們繪制標簽,它將是引腳的文本或引腳的值,這取決于 showvalue 屬性。
我們使用與繪制引腳時類似的策略來繪制文本,但這次我們必須計算水平和垂直偏移量,因為在 microsoft .net 壓縮框架中,drawtext 方法不允許有 textalign 參數。
最終,我們通過調用 dispose 方法清理我們手動使用的 brush 對象。
清單 2 顯示了完整的 ondraw 例程。
清單 2. ondraw() 方法
protected override void onpaint(painteventargs pe)
{
brush b;
// determine the pin color
if(this.enabled)
{
if(type == pintype.digital)
{
// digital pins have different on/off color
b = new system.drawing.solidbrush(
this.value == 0 ? (digitaloffcolor) : (digitaloncolor));
}
else
{
// analog pin
b = new system.drawing.solidbrush(analogcolor);
}
}
else
{
// disabled pin
b = new system.drawing.solidbrush(disabledcolor);
}
// draw the pin
if(this.pinnumber == 1)
pe.graphics.fillrectangle(b, pinbounds);
else
pe.graphics.fillellipse(b, pinbounds);
// draw a border rectangle around the pin
pe.graphics.drawrectangle(new pen(color.black), pinborder);
// draw the text centered in the text bound
string drawstring;
// are we showing the text or value?
if(showvalue)
drawstring = convert.tostring(this.value);
else
drawstring = this.text;
// determine the actual string size
sizef fs = pe.graphics.measurestring(
drawstring,
new font(fontfamily.genericmonospace, 8f,
fontstyle.regular));
// draw the string
pe.graphics.drawstring(
drawstring,
new font(fontfamily.genericmonospace, 8f,
fontstyle.regular),
new solidbrush((showvalue ? analogcolor : color.black)),
textbounds.x + (textbounds.width - fs.tosize().width) / 2,
textbounds.y + (textbounds.height - fs.tosize().height) /
2);
// clean up the brush
b.dispose();
}
}
構建 pin 類的最后一步是添加 click 處理程序。對于我們的 pin 類來說,我們將使用自定義的 eventarg,以便可以向事件處理程序傳遞引腳的文本和編號。要創建自定義的 eventarg,我們只是創建了一個從 eventargs 類派生的類:
public class pinclickeventargs : eventargs
{
// a pinclick passes the pin number and the pin's text
public int number;
public string text;
public pinclickeventargs(int pinnumber, string pintext)
{
number = pinnumber;
text = pintext;
}
}
下一步,我們將一個委托添加到命名空間中:
public delegate void pinclickhandler(pin source, pinclickeventargs args);
現在,我們需要添加代碼來確定什么時候發生單擊,然后引發事件。對于我們的 pin 類,當引腳的邊框矩形內部發生 mousedown 和 mouseup 事件時即為一個邏輯上的單擊 - 這樣,如果用戶單擊引腳的文本部分,則不會觸發 click 事件,但如果點擊表示實際引腳的區域,則觸發該事件。
首先,我們需要一個公共 pinclickhandler 事件,其定義如下:
public event pinclickhandler pinclick;
我們還需要一個私有的布爾變量,我們將在 mousedown 事件發生時設置該變量,用于指示我們正在單擊過程中。然后,我們檢查 mouseup 事件的該變量,以確定事件是否是按連續的順序發生的:
bool midclick;
下一步,我們需要為 mousedown 和 mouseup 添加兩個事件處理程序,如清單 3 所示。
清單 3. 用于實現 pinclick 事件的事件處理程序
private void pinmousedown(object sender, mouseeventargs e)
{
if(!this.enabled)
return;
// if the user clicked in the "pin" rectangle, start a click process
midclick = pinborder.contains(e.x, e.y);
}
private void pinmouseup(object sender, mouseeventargs e)
{
// if we had a mousedown and then up inside the "pin" rectangle,
// fire a click
if((midclick) && (pinborder.contains(e.x, e.y)))
{
if(pinclick != null)
pinclick(this, new pinclickeventargs(
this.pinnumber, this.text));
}
}
最后,我們需要為每個引腳實現事件處理程序。引腳的基本構造函數是添加這些掛鉤的好地方,我們可以通過直接在構造函數中添加以下代碼來完成:
this.mousedown += new mouseeventhandler(pinmousedown);
this.mouseup += new mouseeventhandler(pinmouseup);
實現 pins 類
一旦有了 pin 類,就可以創建從 collectionbase 派生的 pins 類。該類的目的是提供索引器,這樣我們就可以很容易在集合內添加、刪除和操縱 pin 類。
清單 4. pins 類
public class pins : collectionbase
{
public void add(pin pintoadd)
{
list.add(pintoadd);
}
public void remove(pin pintoremove)
{
list.remove(pintoremove);
}
// indexer for pins
public pin this[byte index]
{
get
{
return (pin)list[index];
}
set
{
list[index] = value;
}
}
public pins(){}
}
實現 connector 類
既然我們已經獲得了 pins 類,我們現在需要構建 connector 類,該類將是一個簡單的包裝類,這個包裝類包含 pins 類,并在每個引腳和連接器容器之間封送 pinclick 事件,而且它有一個表示連接器上的引腳數的構造函數。清單 5 顯示了完整的 connector 類。
清單 5. connector 類
public class connector : system.windows.forms.control
{
public event pinclickhandler pinclick;
protected pins pins;
byte pincount;
public connector(byte totalpins)
{
pins = new pins();
pincount = totalpins;
initializecomponent();
}
private void initializecomponent()
{
for(int i = 0 ; i < pincount ; i++)
{
pin p = new pin(pintype.digital,
(pinalignment)((i + 1) % 2), 0);
p.pinclick += new pinclickhandler(onpinclick);
p.pinnumber = i + 1;
p.text = convert.tostring(i);
p.top = (i / 2) * p.height;
p.left = (i % 2) * p.width;
this.pins.add(p);
this.controls.add(p);
}
this.width = pins[0].width * 2;
this.height = pins[0].height * this.pins.count / 2;
}
public pins pins
{
set
{
pins = value;
}
get
{
return pins;
}
}
private void onpinclick(pin sender, pinclickeventargs e)
{
// pass on the event
if(pinclick != null)
{
pinclick(sender, e);
if(sender.type == pintype.digital)
sender.value = sender.value == 0 ? 1 : 0;
else
sender.displayvalue = !sender.displayvalue;
}
}
protected override void dispose( bool disposing )
{
base.dispose( disposing );
}
}
connector 的 initializecomponent 方法是創建所有被包含的 pin 類并將其添加到連接器的控件中的地方,并且是連接器本身調整大小的地方。initializecomponent 也是最終被 form designer 用來顯示我們的連接器的方法。
返回頁首
構建自定義連接器
connector 類本身很簡單,它不會修改任何默認的引腳設置。但是,我們現在可以通過從 connector 類派生新的類,從而構建一個自定義連接器,并修改單個引腳(例如,使某些引腳成為模擬引腳,或將其禁用)。
在示例應用程序中,我為 applied data systems 的 graphics master 板創建了兩個連接器,一個用于 j2,一個用于 j7。構造函數基于連接器設置引腳的類型以及引腳的文本。圖 2 是窗體上有 j2 和 j7 的示例應用程序的屏幕快照。
圖 3. 使用兩個 connector 對象的窗體