March 2005 Archives

有几个公式,不过我的眼睛是感觉不到什么区别咯。

Y = 0.3R + 0.59B + 0.11G

Y=0.212671*R + 0.715160*G + 0.072169*B

Y=(max(R,G,B) + min(R,G,B)) / 2

这是一篇介绍C语言中的函数调用是如何用实现的文章。写给那些对C语言各种行为的底层实现感兴趣人的入门级文章。如果你是C语言或者汇编、底层技术的老鸟或是对这个问题不感兴趣,那么这篇文章只会耽误您的时间,您大可不必阅读他。当然如果前辈们愿意为我指出不足,我将十分感谢您的指导,并对耽误您宝贵的时间致歉。 好了,废话少说!要研究这个问题,让我们先打开VC++吧。最好是6.0的,:-P。(什么你没有VC++,倒!....赶快装一个!@#$,要快!) 首先,让我们在VC++里建立一个Win32 Console Application项目,并建立主文件fun.c。并输入以下内容。

int fun(int a, int b) {
a = 0x4455;
b = 0x6677;
return a + b;
}

int main() {
fun(0x8899,0x1100);
return 0;
}
之后,最关键的是在项目设置里关闭优化功能。也就是把Project->Setting->C/C++->Optimizations选为Disabled。编译器的优化在分析底层实现时大多数情况不太受欢迎。 按键盘上的F10键,进入单步调试模式(Step Over)。看到你的main函数左侧有个黄色的小箭头了吗?那个就是程序即将执行的语句。按Alt + 8。打开反编译窗口,看到汇编语句了吗?是不是想这个样子
==> 00401078   push        1100h
0040107D push 8899h
00401082 call @ILT+5(fun) (0040100a)
00401087 add esp,8
看到两个PUSH指令了吗?再看看后面的数字,不正是我们要传递的参数吗。奇怪阿?我们明明是先传递的0x8899怎么反倒先push 1100h呢?呵呵,这个现象就叫Calling conversion。究竟是何方神圣,我在后面会详细的给你解释的。先别着急。随后的Call指令的作用就是开始调用函数了。 接下来关掉反汇编窗口,在源代码窗口按F11(Step Into)进入函数体。当看到那个黄色的小箭头指向函数名的时候再调出反汇编窗口(Alt+8)。你会看到类似下面的代码:
1:    int fun(int a, int b) {
00401000 push ebp
00401001 mov ebp,esp

00401003 sub esp,40h
00401006 push ebx
00401007 push esi
00401008 push edi
00401009 lea edi,[ebp-40h]
0040100C mov ecx,10h
00401011 mov eax,0CCCCCCCCh
00401016 rep stos dword ptr [edi]
2: a = 0x4455;
00401018 mov dword ptr [ebp+8],4455h
3: b = 0x6677;
0040101F mov dword ptr [ebp+0Ch],6677h
4: return a + b;
00401026 mov eax,dword ptr [ebp+8]
00401029 add eax,dword ptr [ebp+0Ch]


5: }
0040102C pop edi
0040102D pop esi
0040102E pop ebx
0040102F mov esp,ebp
00401031 pop ebp
00401032 ret

VC++就是好,还在难懂的汇编语句前加入了C语言的源代码。不过同时也有不少我们不需要的代码。因此,你只需要关心红色的部分就可以了。 奇怪阿?不是参数都用push传递了吗?怎么没看到被pop出来?问题其实是这样,当你调用Call进入函数的时候Call背着你做了一件事。call把它下一条语句的地址push进了堆栈。(旁人: 什么!这是为什么?)原因很简单,因为函数调用完了,要用ret返回。而ret怎么知道返回哪里呢?对了, ret指令pop了call指令push给他的地址(搞清楚这个关系哦),然后返回到了这个地址。call和ret配合的如此绝妙,一个PUSH一个POP肯定不会让堆栈不平衡的(老外叫no stack unwinding)。现在明白了,如果你来个pop eax,那eax里面是什么?当然是ret要用的返回地址了。好啦,你要是pop eax就等于抢了ret要用的东西了。不论曾程序流程和道德标准上你做的都不对 :-P。 可是怎么在函数体里使用参数呢?问题其实并不难,既然参数在堆栈里我们就可以使用esp(堆栈指针)来访问了。不过,我相信你也想到了。esp是个经常变化的值。一旦,函数里出现pop或push他就会变化。这样很不容易定位参数的于内存中的位置。因此,我们需要一个不会变化的东西作为访问参数的基准。看看函数体的开头部分:
00401000   push        ebp
00401001 mov ebp,esp
先用push ebp保存了原来ebp的值再把esp的值给ebp。原来ebp就是用来做基准的。也难怪他被称为ebp(Base Pointer)。很自然ret返回前的pop ebp就是恢复原来ebp的数值喽。当然一定要恢复,因为函数里也可以调用函数嘛。每个函数都用ebp,自然要保证使用完后完璧归赵了。现在当函数执行到 mov ebp, esp后堆栈应该变成这个样子了。
/-------------------\  Higher Address
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
+-----------------+
| 函数返回地址 |
| 0x00401087 |
+-----------------+
| ebp |
\-------------------/ Lower Address <== stack pointer
& ebp all point to here, now
由于我们在VC++上使用的int类型是一个32位类型,ebp和函数返回值也是32位的。因此每个量要占去4个字节。另外还需要注意堆栈的扩展方向是高地址到低地址。有了这些指示。我们就可以分析出,第一个参数的地址是ebp + 08h,第二个参数就是ebp + 0ch。看看反汇编的代码:
2:       a = 0x4455;
00401018 mov dword ptr [ebp+8],4455h
3: b = 0x6677;
0040101F mov dword ptr [ebp+0Ch],6677h
与我们的计算吻合。之后呢:
00401031   pop         ebp
00401032 ret
将ebp原来的数值完璧归赵,调用ret指令,ret指令pop出返回地址,之后返回到调用函数的call指令的下一条语句。ret之后,堆栈应该变成这个样子了
/-------------------\  Higher Address
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
\-------------------/ Lower Address <== stack pointer
哈哈,问题出现了,再函数返回后堆栈出现了不平衡的情况(Stack Unwinding)。怎么办呢?好办啊,直接 pop cx pop cx 把堆栈平衡过来就好了。幸好我们只有两个参数,要是有20个的话,那就要有20个pop cx。不说影响美观,程序效率也会很低。所以VC++使用了这个办法解决问题:
00401082   call        @ILT+5(fun) (0040100a)
00401087 add esp,8
看红色的语句,直接将esp的值加8,让堆栈变成
/-------------------\  Higher Address <== stack pointer
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
\-------------------/ Lower Address
通过改变esp从根本上解决了Stack unwinding。(push,pop指令本质上不就是通过改变esp来实现堆栈平衡的吗) 现在,明白了函数如何传递参数,如何调用,如何返回。下一个问题就是看看函数如何传递返回值了。相信你早就注意到了
4:       return a + b;
00401026 mov eax,dword ptr [ebp+8]
00401029 add eax,dword ptr [ebp+0Ch]
可见,函数正式用eax寄存器来保存返回值的。如果你想使用函数的返回值,那么一定要在函数一返回就把eax寄存器的值读出来。至于为什么不用ebx,ecx...,这个虽然没有规定,但是习惯上大家都是用eax的。而且windows程序中也明确指出了,函数的返回值必须放入eax内。 OK,现在来解决什么是calling conversion这个历史遗留问题。如果认真思考过,你一定想函数的参数为什么偏用堆栈转递呢,寄存器不也可以传递吗?而且很快阿。参数的传递顺序不一定要是由后到前的,从前到后传递也不会出现任何问题啊?再有为什么一定要等到函数返回了再处理堆栈平衡的问题呢,能否在函数返回前就让堆栈平衡呢? 所有上述提议都是绝对可行的,而他们之间不同的组合就造就了函数不同的调用方法。也就是你常看到或听到的stdcall,pascal,fastcall,WINAPI,cdecl等等。这些不同的处理函数调用方式就叫做calling convention。 默认情况下C语言使用的是cdecl方式,也就是上面提到的。参数由右到左进栈,调用函数者处理堆栈平衡。如果你在我们刚才的程序中fun函数前加入__stdcall,再来用上面的方法分析一下。
8:        fun(0x8899,0x1100);
00401058 push 1100h ; <== 参数仍然是由右到左传递的
0040105D push 8899h
00401062 call fun (00401000)
;<== 这里没有了 add esp, 08h

1: int __stdcall fun(int a, int b) {
00401000 push ebp
00401001 mov ebp,esp
00401003 sub esp,40h
00401006 push ebx
00401007 push esi
00401008 push edi
00401009 lea edi,[ebp-40h]
0040100C mov ecx,10h
00401011 mov eax,0CCCCCCCCh
00401016 rep stos dword ptr [edi]
2: a = 0x4455;
00401018 mov dword ptr [ebp+8],4455h
3: b = 0x6677;
0040101F mov dword ptr [ebp+0Ch],6677h
4: return a + b;
00401026 mov eax,dword ptr [ebp+8]
00401029 add eax,dword ptr [ebp+0Ch]
5: }
0040102C pop edi
0040102D pop esi
0040102E pop ebx
0040102F mov esp,ebp
00401031 pop ebp
00401032 ret 8; <== ret 取出返回地址后,
; 给esp加上 8。看!堆栈平衡在函数内完成了。
; ret指令这个语法设计就是专门用来实现函数
; 内完成堆栈平衡的
于是得出结论,stdcall是由右到左传递参数,被调用函数恢复堆栈的calling convention. 其他几种calling convention的修饰关键词分别是__pascal,__fastcall, WINAPI(这个要包含windows.h才可以用)。现在,你可以用上面说的方法自己分析一下他们各自的特点了。

.NET Compact Framework中,把原有的Form.Handle属性去掉了。表面上看似乎无法获得一个窗口的句柄了。实际上我们可以变通一下来解决这个问题。首先,通过Form.Capture让窗口获得焦点,接下来使用GetCapture API来取得焦点窗口的句柄。类似的方法还可以获得其他一些有焦点控件的句柄

Code:

[DllImport("coredll.dll")]
private static extern IntPtr SetCapture(IntPtr hWnd);

[DllImport("coredll.dll")]
private static extern IntPtr GetCapture();

public static IntPtr GetHandle()
{
 IntPtr hOldWnd = GetCapture();
 this.Capture = true;
IntPtr ret = GetCapture();
 this.Capture = false;
 SetCapture(hOldWnd);
 return ret;
}

My Understanding of printf of EduOS

| No Comments | No TrackBacks
打开Source Insight来阅读EduOS的源代码,我们在stdio.c里找到了printf的实现代码.首先看看对printf的定义:
  int printf (const char *cntrl_string, ...)
第一个参数cntrl_string是控制字符串,也就是平常我们写入%d,%f的地方.紧接着后面是一个变长参数. 看看函数头部的定义:
  int pos = 0, cnt_printed_chars = 0, i; 
unsigned char* chptr;
va_list ap;
马上晕!除了ap我们可以马上判断出来是用来读取变长参数的,i用于循环变量.其他变量都不知道是怎么回事.不要着急,我们边看代码边分析.代码的第一行必然是
 va_start (ap, cntrl_string);
用来初始化变长参数. 接下来是一个while循环
 while (cntrl_string[pos]) {
...
}
结束条件是cntrl_string[pos]为NULL,显然这个循环是用来遍历整个控制字符串的.自然pos就是当前遍历到的位置了.进入循环首先闯入视线的是
  if (cntrl_string[pos] == '%') {
pos++;
...
}
开门见山,上来就当前字符是否办断是否%.一猜就知道如果成立pos++马上取出下一个字符在d,f,l等等之间进行判断.往下一看,果真不出所料:
switch (cntrl_string[pos]) {
case 'c':
...
case 's':
...
case 'i':
...
case 'd':
...
case 'u':
...
用上switch-case了. 快速浏览一下下面的代码. 首先看看case 'c'的部分
case 'c':
putchar (va_arg (ap, unsigned char));
cnt_printed_chars++;
break;
%c表示仅仅输出一个字符.因此先通过va_arg进行参数的类型转换,之后用putchar[1]输出到屏幕上去.之后是 cnt_printed_chars++,通过这句我们就可以判断出cnt_printed_chars使用来表示,已经被printf输出的字符个数的. 再来看看 case 's':
case 's':
chptr = va_arg (ap, unsigned char*);
i = 0;
while (chptr [i]) {
cnt_printed_chars++;
putchar (chptr [i++]);
}
break;
和case 'c',同出一辙.cnt_printed_chars++放在了循环内,也证明了刚才提到的他的作用.另外我们也看到了cnptr是用来在处理字符串时的位置指针.到此为止,我们清楚的所有变量的用途,前途变得更加光明了. 接下来:
// PartI
case 'i':
case 'd':
cnt_printed_chars += printInt (va_arg (ap, int));
break;
case 'u':
cnt_printed_chars += printUnsignedInt (va_arg (ap, unsigned int));
break;
case 'x':
cnt_printed_chars += printHexa (va_arg (ap, unsigned int), 'x');
break;
case 'X':
cnt_printed_chars += printHexa (va_arg (ap, unsigned int), 'X');
break;
case 'o':
cnt_printed_chars += printOctal (va_arg (ap, unsigned int));
break;
// Part II
case 'p':
putchar ('0');
putchar ('x');
cnt_printed_chars += 2; /* of '0x' */
cnt_printed_chars += printHexa (va_arg (ap, unsigned int), 'x');
break;
case '#':
pos++;
switch (cntrl_string[pos]) {
case 'x':
putchar ('0');
putchar ('x');
cnt_printed_chars += 2; /* of '0x' */
cnt_printed_chars += printHexa (va_arg (ap, unsigned int), 'x');
break;
case 'X':
putchar ('0');
putchar ('X');
cnt_printed_chars += 2; /* of '0X' */
cnt_printed_chars += printHexa (va_arg (ap, unsigned int), 'X');
break;
case 'o':
putchar ('0');
cnt_printed_chars++;
cnt_printed_chars += printOctal (va_arg (ap, unsigned int));
break;
注意观察一下,PartII的代码其实就是比PartI的代码多一个样式.在16进制数或八进制前加入0x或是o,等等.因此这里就只分析一下PartI咯. 其实仔细看看PartI的个条case,也就是把参数分发到了更具体的函数用于显示,然后以返回值的形式返回输出个数.对于这些函数就不具体分析了.我们先来看看一些善后处理: 先看case的default处理.
default:
putchar ((unsigned char) cntrl_string[pos]);
cnt_printed_chars++;
就是直接输出cntrl_string里%号后面的未知字符.应该是一种容错设计处理. 再看看if (cntrl_string[pos] == '%')的else部分
      else {
putchar ((unsigned char) cntrl_string[pos]);
cnt_printed_chars++;
pos++;
}
如果不是%开头的,那么直接输出这个字符. 最后函数返回前
  va_end (ap);
return cnt_printed_chars;
va_end处理变长参数的善后工作.并返回输出的字符个数. 在最后我们有必要谈谈putChar函数以及基本输出的基础函数printChar,先来看看putChar
 int putchar (int c) {
switch ((unsigned char) c) {
case '\n' :
newLine ();
break;
case '\r' :
carriageReturn ();
break;
case '\f' :
clearScreen ();
break;
case '\t' :
printChar (32); printChar (32); /* 32 = space */
printChar (32); printChar (32);
printChar (32); printChar (32);
printChar (32); printChar (32);
break;
case '\b':
backspace ();
break;
case '\a':
beep ();
break;
default :
printChar ((unsigned char) c);
}
return c;
}
通览一下,也是switch-case为主体的.主要是用来应对一些特殊字符,如\n,\r,....这里需要提一下,关于\t的理解.有些人认为\t就是8个space,有些人则认为,屏幕分为10大列(每个大列8个小列总共80列).一个\t就跳到下一个大列输出.也就是说不管你现在实在屏幕的第1,2,3,4,5,6,7位置输出字符,只要一个\t都在第8个位置开始输出. VS.NET中就是用的这种理解.因此如果按照这个理解的话,\t的实现可以这样
 int currentX = ((currentX % 10) + 1) * 8;
然后在currentX位置输出. 接下来看printChar也就是输出部分最低层的操作咯
 void printChar (const byte ch) {
*(word *)(VIDEO + y * 160 + x * 2) = ch | (fill_color << 8);
x++;
if (x >= WIDTH)
newLine ();
setVideoCursor (y, x);
}
这里VIDEO表示显存地址也就是0xB8000.通过 y * 160 + x 屏幕(x,y)坐标在显存中的位置.这里需要知道,一个字符显示需要两个字节,一个是ASCII码,第二个是字符属性代码也就是颜色代码.因此才必须 y * 80 * 2 + x = y * 160 + x.那么ch | (fill_color << 8)也自然就是写入字符及属性代码用的了.每写一个字符光标位置加1,如果大于屏幕宽度WIDTH就换行.最后通过setVideoCursor设置新的光标位置.完成了整个printChar过程. 到此,把printf从上到下说了一遍.不知道各位大家感觉如何,如果说得不清楚还大家多提意见.有说得不对的地方请大家多多指教.
物理连接


把ADSL MODEM接在集线器的普通端口上,其他机器也接在集线器上


ADSL MODEM设置



先设置一台机器的静态IP为10.0.0.*(不是2就行),子网掩码为255.255.255.0.


打开机器的浏览器,输入http://10.0.0.2这时会弹出一个密码框。用户名/密码: admin/epicrouter。

进入管理页面后选择广域网配置,选择PVC0然后提交。
o 更改下列设置:

+ 桥:关闭

+ 封装协议:PPoE LLC

+ 网关:192.168.241.101

+ 在PPP设置里输入上网账号和密码。

+ Note: 其他设置千万不要更改

o 提交,然后保存设置。保存设置并重起。
启动后,进入管理网页PPP页面,选择 PVC0 所在的连接然后点连接。等状态变为Connected之后就代表ADSL 路由器连接到Internet了。
之后把子网机器的IP改为10.0.0.*(不是2就行);子网掩码255.0.0.0;网关10.0.0.2;DNS:202.106.0.20
同时管理页面还可以设置DHCP以及端口映射。
268L ADSL Modem Settings



Control Panel -> Regional and Language Options ->

  • 在Regional Options选项卡里把Standards and formats 选为 Chinese(PRC) 使用中国习惯的单位表示方法。再把Location改为中文。
  • 在Languages选项卡里给Install files for East Asian languages复选框打勾,插入Windows XP 光盘安装东方语言包。(这样就可以显示中文了)。
  • 在 Advanced选项卡里把Language for non-Unicode programs语言选择为Chinese(PRC)确保一些中文的Ansi程序可以正确显示。为Default user account settings打勾将把这个设置应用到默认帐户中。

注:对于non-Unicode语言,如果选择为Chinese一些Microsoft的CHM电子书中的代码部分可能出现乱码。这是需要将IE的Encoding改在UTF-8或者Western 选项上,来解决这一问题。

Control Panel | Regional and Language Options |

  • 在Regional Options选项卡里把Standards and formats 选为 Chinese(PRC) 使用中国习惯的单位表示方法。再把Location改为中文。
  • 在Languages选项卡里给Install files for East Asian languages复选框打勾,插入Windows XP 光盘安装东方语言包。(这样就可以显示中文了)。
  • 在Advanced
    选项卡里把Language for non-Unicode
    programs语言选择为Chinese(PRC)确保一些中文的Ansi程序可以正确显示。为Default user account

    settings打勾将把这个设置应用到默认帐户中。

注:对于non-Unicode语言,如果选择为Chinese一些Microsoft的CHM电子书中的代码部分可能出现乱码。这是需要将IE的Encoding改在UTF-8或者Western 选项上,来解决这一问题。

HWND onOpenCamera(HWND hwnd) {
 TCHAR szDeviceName[255];
 TCHAR szDeviceVersion[255];
 for (int wIndex = 0; wIndex < 10; wIndex++)
 {
  if (
       capGetDriverDescription (
      wIndex
      , szDeviceName
      , sizeof (szDeviceName)
      , szDeviceVersion
      , sizeof (szDeviceVersion)
    )
   ) {
   // Append name to list of installed capture drivers
   // and then let the user select a driver to use.
   MessageBox(hwnd, szDeviceVersion, szDeviceName , MB_OK);
  }
 }


 hWndC = capCreateCaptureWindow (
  (LPSTR) "My Capture Window", // window name if pop-up
  WS_CHILD | WS_VISIBLE,       // window style
  120, 200, 92, 112,              // window position and dimensions
  (HWND) hwnd,
  (int) 1 /* child ID */);

 //fOK = SendMessage (hWndC, WM_CAP_DRIVER_CONNECT, 0, 0L);
 //
 // Or, use the macro to connect to the MSVIDEO driver:
 int  fOK = capDriverConnect(hWndC, 0);

 
 //
 // Place code to set up and capture video here.
 //


 CAPDRIVERCAPS CapDrvCaps;

 capDriverGetCaps(hWndC, &CapDrvCaps, sizeof (CAPDRIVERCAPS));
 
 // Video source dialog box.
 if (CapDrvCaps.fHasDlgVideoSource)
  capDlgVideoSource(hWndC);
 
 // Video format dialog box.
 if (CapDrvCaps.fHasDlgVideoFormat)
 {
  capDlgVideoFormat(hWndC);

  CAPSTATUS CapStatus;

  // Are there new image dimensions?
  capGetStatus(hWndC, &CapStatus, sizeof (CAPSTATUS));

  // If so, notify the parent of a size change.
 }
 
 // Video display dialog box.
 if (CapDrvCaps.fHasDlgVideoDisplay)
  capDlgVideoDisplay(hWndC);
 
//  BITMAPINFO bi;
// capSetVideoFormat(hWndC, &bi, sizeof(BITMAPINFO));
 
 
 capPreviewRate(hWndC, 1);     // rate, in milliseconds
 capPreview(hWndC, TRUE);       // starts preview

 return hWndC;
}

Introduction to Making .NET CF Custom Control

| No Comments | No TrackBacks

Visual Studio .NET 2003 Windows Mobile用户提供的控件大多不太理想。有些是样式不大如意。更多时候是我们找不到一个能满足我们功能的控件。比如带有图片的ListBox, 带有CheckBoxListBox,带有IconMenu等等。(同时也奇怪为什么M$不把Smartphone里显示短信的控件放在ToolBox)

因此自定义控件(Custom Control)。几乎就是.NET CF开发的必修课了。在这片文章里,通过制作一个简单的按钮控件来说明自定义控件的基本步骤。

public class CustomControl: System.Windows.Forms.Control

{

   // ...

protected override void OnPaint(PaintEventArgs e)

   {

       // DO SOMETHING

}

   protected override void OnPaintBackground(PaintEventArgs pevent)

   {

      // DO NOTHING

   }

   // ...

}


由于.NET CF并不支持控件的OwnerDraw机制,所以要想自定义控件只能派生自System.Windows.Forms.Control了。然后白手起家,慢慢搭建自己的控件库。

Windows.Forms.Control派生出自己的CustomControl。之后还有重载OnPaintOnPaintBackground两个方法,对控件和控件背景进行高效的绘制。因为.NET CF是运行在移动设备之上,因此绘图的效率对显示的影响是十分明显的。为了提高效率我们从以下几个方面着手:

l         使用缓冲区绘图。将绘制过程放在缓冲区内进行。需要绘制控件时,直接将缓冲区内容全部绘出。

l         努力将绘图运算放置在OnPaintOnPaintBackground函数之外。

l         充分利用类继承特性,避免重复绘制。

l         避免在OnPaintOnPaintBackground函数内产生和销毁对象。

//...

protected Bitmap m_bmpBuffer = null;

protected Graphics m_gxBuffer = null;

//...

protected override void OnResize(EventArgs e)

{

   // Dispose Old Graphic Buffer

   if (this.m_bmpBuffer != null)

      this.m_bmpBuffer.Dispose();

   if (this.m_gxBuffer != null)

      this.m_gxBuffer.Dispose();

   m_borderRect = new Rectangle

      (

      this.ClientRectangle.Left

      ,this.ClientRectangle.Top

      ,this.ClientRectangle.Right - 1

      ,this.ClientRectangle.Bottom - 1

      );

 

   this.m_bmpBuffer = new Bitmap

      (

         this.ClientRectangle.Width

         , this.ClientRectangle.Height

      );

 

   this.m_gxBuffer = Graphics.FromImage(this.m_bmpBuffer);

   this.Invalidate();

   base.OnResize (e);

}


 

上面的代码中,开辟了一个m_gxBuffer 缓冲区。并通过重载OnResize函数来调整缓冲区的大小用以适应控件的大小。

protected override void OnPaint(PaintEventArgs e)

{

   if (Enabled)

 

   {

      Pen borderPen = new Pen(m_borderColor);

      m_gxBuffer.Clear(this.BackColor);

      if (m_border)

         m_gxBuffer.DrawRectangle(borderPen, m_borderRect);

   }

 

   else

 

   {

      Pen disablePen = new Pen(Color.DarkGray);

      m_gxBuffer.Clear(Color.Gray);

      if (m_border)

         m_gxBuffer.DrawRectangle(disablePen, m_borderRect);

   }

 

if (m_bufferOutput)

      e.Graphics.DrawImage(m_bmpBuffer,0,0);

}


之后在OnPaint中就开始使用m_gxBuffer来进行缓冲区绘图了。当然在整个OnPaint结束之前应该调用e.Graphics.DrawImage将缓冲区输出到屏幕上。很快一个控件的雏形就出现了。现在给他增加一些功能让他看起来更像是个按钮(而不是现在这个丑样子)

 

private bool m_active = false;

private Color m_activeBackColor = Color.LightGray;

private Color m_activeForeColor = Color.FromArgb(74,97,148);


首先我们给控件增加一些有用的属性。比如我们要判断按钮是否被按下了。以及按下按钮时,按钮应该是什么样子的。(最简单的就是改变他的颜色了吧)

不要过多考虑按钮可能出现的状态。比如在普通的使用触摸屏的移动设备上,我们根本无需把Hover特性考虑在内。因为凭借Windows CE你永远不会知道你的笔是否在一个按钮的上方。

几条简单的if语句就可以把状态的判断完全搞定

protected override void OnMouseMove(MouseEventArgs e)

{

   if (e.Button == MouseButtons.Left)

 

   {

      if (this.m_active

         && this.ClientRectangle.Contains(e.X, e.Y) == false)

 

      {

         this.m_active = false;

         this.Invalidate();

      }

 

      else if (!this.m_active

         && this.ClientRectangle.Contains(e.X, e.Y) == true)

 

      {

         this.m_active = true;

         this.Invalidate();

      }

   }

   base.OnMouseMove (e);

}

protected override void OnMouseDown(MouseEventArgs e)

{

   if (e.Button == MouseButtons.Left)

 

   {

      this.m_active = true;

      this.Focus();

      this.Invalidate();

   }

   base.OnMouseDown (e);

}

protected override void OnMouseUp(MouseEventArgs e)

{

   if (e.Button == MouseButtons.Left)

 

   {

      this.m_active = false;

      this.Invalidate();

   }

   base.OnMouseUp (e);

}


 

这样就可以准确的判断出按钮是否被点击(Click)

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)

{

// ...

if (this.m_active)

 

   {

      m_gxBuffer.Clear(this.m_activeBackColor);

      m_gxBuffer.DrawRectangle(activePen, m_borderRect);

  

 

      if (this.Text.Length > 0)

 

      {

         m_gxBuffer.DrawString

            (

            this.Text

            , this.Font, activeForeBrush

            ,  this.TextPosX+ 2

            ,  this.TextPosY+ 1

            );

      }

   }

 

   else

 

   {

      base.OnPaint(e);

   }

// ...

}


好了,现在来看看OnPaint,他利用上面得到的m_active来区分绘制那种状态的按钮。

 

一个按钮就算基本完成了,下面我们要做的工作就是如何把它加入到我们的ToolBox中,以便随时使用。

由于.NET 对于Design Time(设计时期设计器里看到的控件) Runtime(程序运行时看到的控件)的控件需要分别用不同的方式编译。所以要采用宏定义作为开关来控制一些特性。

首先,DesignTime的控件必须包含System.ComponentModel命名空间。

#if NETCFDESIGNTIME

using System.ComponentModel;

#endif


 

 

为了能在设计器里设置控件的属性,还要对这些加以特殊的声明。例如BorderColor属性

#if NETCFDESIGNTIME

        [System.ComponentModel.Category("Appearance")]

        [System.ComponentModel.Description("Border Color")]

#endif

public Color BorderColor

 

{

   get

 

   {

      if (m_borderColor == Color.Empty)

         m_borderColor = Color.FromArgb(165,165,165);

      return this.m_borderColor;

   }

   set

 

   {

      this.m_borderColor = value;

      this.Invalidate();

   }

}


这里System.ComponentModel.Category表示属性的类别,比如Appearance就是外观类。下面的Description看字面就知道是属性的描述了。此外还可以通过加System.ComponentModel.DefaultValue来设置属性的默认值。但是这里DefaultValue只能是一些基本类型。如果是特殊类型比如Color之类,提前初始化就可以了。另外,还可以设置控件的默认处理事件。也就是当在DesignTime双击这个控件时自动安排的处理事件。比如

#if NETCFDESIGNTIME

   [System.ComponentModel.DefaultProperty("Text")]

   [System.ComponentModel.DefaultEvent("Click")]

#endif


 

 

之后进入.NET 2003的命令行窗口进行DesignTime控件的编译

csc /noconfig /define:NETCFDESIGNTIME /target:library /out:CustomButton.dll Button.cs /res:"Bitmaps\CustomControl.Button.bmp"
/r:"C:\Program Files\Microsoft Visual Studio .NET 2003\CompactFrameworkSDK\v1.0.5000\Windows CE\Designer\System.CF.Design.dll"
/r:"C:\Program Files\Microsoft Visual Studio .NET 2003\CompactFrameworkSDK\v1.0.5000\Windows CE\Designer\System.CF.Windows.Forms.dll"
/r:"C:\Program Files\Microsoft Visual Studio .NET 2003\CompactFrameworkSDK\v1.0.5000\Windows CE\Designer\System.CF.Drawing.dll"
/r:System.Windows.Forms.dll /r:System.Drawing.dll
/r:System.dll /r:System.XML.dll /r:System.Web.Services.dll
/r:System.Data.dll /nowarn:1595


 

这里可以添加一个和你控件命名控件同名的BMP文件用来做控件在ToolBox上显示的图标。不然的话,.NET IDE会用一个难看的齿轮代替的。BMP可以用.NET的资源编辑器创建。格式为16x16x256.

 

最后在.NET IDEToolBox中添加自己的控件就可以使用了。

Visual Studio .NET 2003 Windows Mobile用户提供的控件大多不太理想。有些是样式不大如意。更多时候是我们找不到一个能满足我们功能的控件。比如带有图片的ListBox, 带有CheckBoxListBox,带有IconMenu等等。(同时也奇怪为什么M$不把Smartphone里显示短信的控件放在ToolBox)

因此自定义控件(Custom Control)。几乎就是.NET CF开发的必修课了。在这片文章里,通过制作一个简单的按钮控件来说明自定义控件的基本步骤。

publicclass CustomControl: System.Windows.Forms.Control

{

// …

protectedoverridevoid OnPaint(PaintEventArgs e)

{

// DO SOMETHING

}

protectedoverridevoid OnPaintBackground(PaintEventArgs pevent)

{

// DO NOTHING

}

// …

}

由于.NET CF并不支持控件的OwnerDraw机制,所以要想自定义控件只能派生自System.Windows.Forms.Control了。然后白手起家,慢慢搭建自己的控件库。

Windows.Forms.Control派生出自己的CustomControl。之后还有重载OnPaintOnPaintBackground两个方法,对控件和控件背景进行高效的绘制。因为.NET CF是运行在移动设备之上,因此绘图的效率对显示的影响是十分明显的。为了提高效率我们从以下几个方面着手:

l使用缓冲区绘图。将绘制过程放在缓冲区内进行。需要绘制控件时,直接将缓冲区内容全部绘出。

l努力将绘图运算放置在OnPaintOnPaintBackground函数之外。

l充分利用类继承特性,避免重复绘制。

l避免在OnPaintOnPaintBackground函数内产生和销毁对象。

//…

protected Bitmap m_bmpBuffer = null;

protected Graphics m_gxBuffer = null;

//…