Friday, May 28, 2004

 

Some notes on Skin

1.
关于如何换肤、子类化的解决方案的讨论

The author is unknown

对于应用程序的换肤及子类化。下面是我尝试过一些方法,以在CAboutDlg中子类化其中的Button为例:
第一种:直接用现成的类
1.自己写一个类class CButtonXP : public CButton{/*...*/}
用MessageMap处理感兴趣的消息。
2.用CButtonXP代替CButton来声明变量m_btn;

3.在void CAboutDlg:oDataExchange(CDataExchange* pDX)中加上一句:
DDX_Control(pDX, IDB_BUTTON1, m_edit);
或者在InitDialog()中加上
m_btn.SubclassDlgItem(IDB_BUTTON1, this);
这两种效果差不多的。

第二种:在Hook中使用现成的类。
1.自己写一个类class CButtonXP : public CButton{/*...*/}
用MessageMap处理感兴趣的消息。
2,用g_hWndProcHook = ::SetWindowsHookEx(WH_CALLWNDPROC,
WndProcHook, NULL, ::GetCurrentThreadId());安装一个钩子
3. 在WndProcHook中处理窗口创建和销毁的消息
LRESULT CALLBACK WndProcHook(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION)
{
switch (((CWPSTRUCT*) lParam)->message)
{
case WM_CREATE:
BeginSubclassing(((CWPSTRUCT*) lParam)->hwnd);
break;

case WM_NCDESTROY:
// TODO: clear subclass info.
EndSubclassing(((CWPSTRUCT*) lParam)->hwnd);
break;

default:
break;
}
}

return CallNextHookEx(g_hWndProcHook, code, wParam, lParam);
}
4. 在BeginSubclassing中用GetClassName得到类名,例如"Button",然后用CButtonXP类进行子类化.
CButtonXP pButton = new CButtonXP
VERIFY(pButton ->SubclassWindow(hWnd));

第三种 在Hook中使用窗口过程。
1. 自己写一个按钮的窗口过程
WNDPROC oldProc;
LRESULT CALLBACK ProcButton(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
ASSERT(oldProc != 0);
if (oldProc == 0) return TRUE;
switch (uMsg)
{
case WM_ERASEBKGND:
break;
//......
default:
break;
}

return CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam);
}
2.同第二种
3.同第二种
4.在BeginSubclassing中得到类名后,用SetWindowLong的方式子类化
oldProc = (WNDPROC) GetWindowLong(hWnd, GWL_WNDPROC);
SetWindowLong(hWnd, GWL_WNDPROC, (LONG) ProcButton);

第四种:不用Hook
在一个对话框的OnInitDialog中枚举它的所有子窗体
例如用下面两句来实现
hWnd=GetWindow(hDlg,GW_CHILD);
hWnd=GetWindow(hWnd,GW_HWNDNEXT);
对每个子窗体进行子类化处理,处理过程同第二种与第三种。

第五种:如果是在XP下运行,可以使用manifest,也就是如下的一个XML文件


name="Microsoft.Windows.XXXX"
processorArchitecture="x86"
version="5.1.0.0"
type="win32"/>
Windows Shell


type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="x86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>



把它存为 应用程序名.manifest,放到和应用程序对应的目录下
或者把它作为资源类型为24的资源编译进应用程序中。
这样程序在XP下就自动拥有了XP的风格。

第六种:使用第三方的库Skin++(www.uipower.com)实现换肤。
第七种:用第三方应用程序给整个windows换肤(windowblinds)。

以上七种方式各有优缺点。我在使用过程中也遇到不少问题,现在一一道来,希望和大家共同解决问题。
先排除几种不准备深入探讨的方式:
第五种 manifest方式 最快速和简洁,但是功能有限,存在严重的平台限制,
不过好处在于应用程序可以和windows共一种风格。
第六种 使用第三方的库Skin++(www.uipower.com)实现换肤方式 使用起来很简单,定制性也不错, 可供选择的皮肤种类非常的多, 支持的语言非常广泛,可以称得上是换肤功能的终结者,对于共享软件开发者和 注重界面的企业来说是个不错的解决方案,他的换肤理念很新,有些地方做得很独特,比如可以对BCG换肤等,有些技术点,很多同类产品都没有做到,比如ComboBox的滚动条,系统对话框(open or close Dialog)的菜单等等。
第七种, 属于自娱性质的,也就不多说了。
第一种, 直接使用现成的类,属于很常见的一种用法,一般来说使用上不会出什么问题,缺点就不说了,
如果这种方式让我满意,我就不必发这篇帖子了。

下面看看第二三四种:
第二种是用HOOK+窗口类,实现起来比较方便,和做一个自绘控件的工作量其实是一样的。
第三种是用HOOK+窗口过程,实现起来比较麻烦,需要自己处理一堆switch case, 自己转换消息参数,
自己找地方维护一堆状态变量,工作量很大。
第四种不用HOOK的方式,有个缺点:对被换肤的程序的源代码的修改比较多。当然,直接到进程中去找窗口句柄,
然后子类化那么就不用源代码了,不过这样的话还不如用HOOK呢。
实际上,HOOK机制和枚举窗体虽然过程不同,不过最终目的是一样的,都是为了子类化窗口。所以在此不去探讨孰优孰劣了。

现在切入正题,谈谈在子类化过程中遇到的问题:
重复subclass的问题
上面提到,子类化的两种方式:用窗口类或者用窗口过程。
使用窗口类是从CWnd派生一个类,调用CWnd的protected函数SubclassWindow.
可是如果正常使用一个窗口类(声明成员变量,加入DDX_Control),实际上在DDX_Control中也是是用了SubclassWindow的。
假如为一个控件声明变量,而在Hook中又进行了子类化,结果会怎么样呢?
答案是,程序崩溃或弹出消息框"不支持的操作"。
因为SubclassWindow函数调用前是要先Attach到一个HWND上去的。重复的Attach看来是不允许。
要避免程序崩溃也有办法:
1. 只为控件声明一个指针变量,动态的去获取CWnd类的实例,但是这样就达不到换肤的目的了。
2. 还有一种方法,经过我试验,如果两个SubclassWindow的调用位于不同的模块,例如一个位于exe,一个位于dll(我是通过exe中调用dll中的函数显示该dll中的对话框来测试的),那么就不会出现问题。在还没有找到更好的方法之前,这也姑且算是一种解决方法吧。

但是如果使用窗口过程来子类化,就不存在重复subclass的问题了,只要小心处理,子类化无数次都没问题,但是对于复杂的自绘事件,在一个窗口过程中来写switch语句,好像很麻烦。
我尝试过自己写一个新的SubclassWindow函数来尝试借用CWnd的窗口过程,这样就可以按照MFC的方式来写消息响应函数了。只可惜,最终还是无功而返,因为SubclassWindow不是虚函数,而CWnd的窗口过程是作为一个protected成员存在的。所以没法在外部借用MFC的消息机制。
所以,自己写代码处理wParam和lParam看来在所难免。

子类化系统对话框的问题。
系统的对话框和自己的对话框表现的总不一样。
目前我还没有对所有的系统对话框进行测试。在MessageBox弹出的对话框中遇到的问题可以见我这一片帖子:
http://community.csdn.net/Expert/To....asp?id=3103399
在文件对话框中我遇到一个问题,子类化过的CStatic的背景好像没有重绘一样,照理说应该由CStatic的父窗体负责背景的。
我在我的CStaticNew类中只重载了OnPaint,里面只处理文字和图标的绘制,背景的绘制留给父窗体完成。这样的处理在MessageBox和自己的AboutDlg中都没有问题,Static控件的背景就是父窗口的背景,可是在CFileDlg中背景就没有重绘了
void CStaticNew::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: Add your message handler code here
CRect rt;
GetWindowRect(rt);

// 绘制背景

dc.SetBkMode(TRANSPARENT);
// 绘制文字
CFont *pfont, * pOldFont;
pfont = GetFont();
if (pfont)
pOldFont = dc.SelectObject(pfont);

CString szTitle;
GetWindowText(szTitle);
dc.DrawText(szTitle, CRect(0, 0, rt.Width(), rt.Height()), DT_LEFT | DT_WORDBREAK );

if (pfont)
dc.SelectObject(pOldFont);
// 绘制图标
if ((GetStyle() & SS_ICON) != 0)
{
dc.DrawIcon(0, 0, GetIcon());
}
// Do not call CStatic::OnPaint() for painting messages
}

类名的识别问题
到现在为止,我所使用的子类化方法都是基于GetClassName这个函数获得窗口类名,再根据用spy++所得到的知识,
如"#32770"表示对话框,"ToolbarWindow32"是工具栏,等等。但是窗口类名是可以在创建时任意指定的呀,
而像CMainFrame的类名根本就不能够确定,例如记事本主窗体的类名是"Notepad",写字板主窗体的类名是"WordPadClass"
这样的话,子类化如何去进行呢。真想知道windows是怎么做的,skinmagic又是怎么做的。

目前主要就是这三个问题了。
希望大家能展开讨论,给出一个换肤的完善的解决方案

我写了一个简化的CWnd类来解决重复子类化问题和简化窗口过程,不过它不支持对自己的重复子类化(即只能用于没有被子类化的或者被CWnd子类化的HWND)。
因为不想弄得和MessageMap那样复杂,所以功能也有限:须手工转化WPARAM和LPARAM、消息处理无法继承、不支持多线程。

使用很简单,CWndNew* pWnd = new CWndNew;
pWnd->SubclassWindow(hWnd);

用完了,记得pWnd->UnsubclassWindow();和delete pWnd;
如果要进行功能扩充(继承),就改写那几个虚函数

class CWndNew
{
public:
CWndNew();
virtual ~CWndNew();
bool SubclassWindow(HWND hWnd);
void UnsubclassWindow();
protected: // virtual
virtual LRESULT WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam);
virtual void PresubclassWindow(){};
virtual void PostunsubclassWindow(){};

protected:
LRESULT PrevWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam);
HWND m_hWnd;
private:
WNDPROC m_oldProc;
static map m_map;
static LRESULT CALLBACK StaticWindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
};

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
map CWndNew::m_map;
CWndNew::CWndNew()
{
m_hWnd = NULL;
}

CWndNew::~CWndNew()
{
ASSERT(m_hWnd == NULL);
}
bool CWndNew::SubclassWindow(HWND hWnd)
{
m_map[hWnd] = this;
ASSERT(m_hWnd == NULL);
m_hWnd = hWnd;
//允许派生类在子类化之前做一些初始化.
PresubclassWindow();

m_oldProc = (WNDPROC) GetWindowLong(hWnd, GWL_WNDPROC);
ASSERT(m_oldProc != 0);
SetWindowLong(hWnd, GWL_WNDPROC, (LONG) StaticWindowProc);
return true;
}

void CWndNew::UnsubclassWindow()
{
SetWindowLong(m_hWnd, GWL_WNDPROC, (LONG)m_oldProc);
PostunsubclassWindow();
m_map.erase(m_hWnd);
m_hWnd = NULL;
}

LRESULT CALLBACK CWndNew::StaticWindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
CWndNew* pWnd = m_map[hWnd];
ASSERT(pWnd != NULL);
return pWnd->WindowProc(uMsg, wParam, lParam);
}

LRESULT CWndNew::PrevWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return CallWindowProc(m_oldProc, m_hWnd, uMsg, wParam, lParam);
}

LRESULT CWndNew::WindowProc(
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
return PrevWindowProc(uMsg, wParam, lParam);
}
关于子类化及其撤销的顺序问题:

当用自己的类或者过程子类化窗口时,需要处理好与MFC类子类化的顺序冲突。
假设我们自己的类叫CWndNew,那么不管CWnd和CWndNew谁先子类化一个窗口,最终两者

协同工作的结果应该是该窗口的窗口过程还原到未子类化之前的状态。
首先,不要在HOOK过程中处理WM_NCDESTROY消息。
理由:如果CWndNew比CWnd先子类化,由于HOOK的原因,你仍然会先处理WM_NCDESTROY

,这时候如果你撤销子类化,那么CWnd类就得不到机会清理。而如果你不撤销子类化

,CWnd没有能力把被子类化的窗口还原到最初状态。在HOOK过程中,不能通过调用

SendMessage函数让CWnd先行处理,然后你自己再处理,因为SendMessage后,消息又会被HOOK拦截。

由于上述原因,在CWndNew的消息过程中处理WM_NCDESTROY是很不错的选择,MFC也是这样做的。
参照如下的代码进行解释:
case WM_NCDESTROY:
{
LRESULT lret;
WNDPROC wndproc;
wndproc = (WNDPROC)GetWindowLong(m_hWnd, GWL_WNDPROC);
if (wndproc == CWndNew::StaticWindowProc)
{
HWND hWnd = m_hWnd;
UnsubclassWindow();
lret = CallWindowProc(m_oldProc, hWnd, uMsg, wParam, lParam);
}
else
{
lret = CallWindowProc(m_oldProc, m_hWnd, uMsg, wParam, lParam);
if(wndproc == (WNDPROC)GetWindowLong(m_hWnd, GWL_WNDPROC))
UnsubclassWindow();
}
delete this;
return lret;
}
首先判断该窗口的WNDPROC是否发生过变动,如果没有的话是最好的,赶紧撤销子类化,再把消息传递给之前窗口过程,然后功成身退,不问世事了。
如果发生过变动,那么也就是说有别的类在CWndNew子类化以后又进行了子类化,而现在又把WM_NCDESTROY传给了CWndNew。这好办,如法炮制,把消息继续往前传,如果WNDPROC又发生了改变,说明之前的某个窗口过程已经作了处理,就不需要再进行撤销子类化的操作了。这点MFC的CWnd类也是这样做的。


另外还有一个问题不解:
就是Edit,ListBox,ListCtrl等等控件的内嵌的滚动条是怎么换肤的?
网上一般介绍的方法是隐藏原来的,然后换上自己重新实现的。
这种在Spy++中一看就能现出原形,可是Skin++ 换肤后的滚动条就不知道是怎么实现的了,不知有谁知道?

2.
SkinControls 1.1 - A journey in automating the skinning of Windows controls
By .dan.g.

A self-contained, user-extensible, application-wide skinning architecture for Windows controls
http://www.codeproject.com/useritems/SkinCtrl.asp

3.
Effecto Player
By Ahmed Ismaiel Zakaria

Media audio player with 3D and 2D effects and skinning.
http://www.codeproject.com/audio/Effecto.asp

4.
作者:易剑

为了使每个窗体的标题栏都能定制,并且不用为每一个窗体类编码,所以本方法采用钩子技术,其核心思想是监控 Windows 消息,处理需要重缓标题的消息,以达到定制标题栏的思想.
本文件介绍的方法将在应用程序中安装 WH_CALLWNDPROC 钩子,具体的代码如下所示:

在应用程序启动时安装钩子的代码:
extern "C" BOOL __declspec(dllexport) InstallCallWndHook()
{
g_hCallWndProc = SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, AfxGetInstanceHandle(), GetCurrentThreadId());
if (NULL == g_hCallWndProc)
return FALSE;
else
return TRUE;
}

InstallCallWndHook() 函数定义为出口函数,在需要定制标题的程序中将调用它,由于安装的是 WH_CALLWNDPROC 钩子,所以在应用程序调用自己的窗体过程之前,总会先调用 CallWndProc;如果设置为 WH_CALLWNDPROCRET 则顺序刚好相反
g_hCallWndProc为一内存共享变量,其它定义的方法如下所示:

#pragma data_seg("Shared")
static HHOOK g_hCallWndProc;
#pragma data_seg()

当然你也可以定义为其它形式,比如直接采用共享内存API创建方式.

在应用程序退出时安装卸载钩子的代码:
extern "C" void __declspec(dllexport) UnInstallCallWndHook()
{
if (g_hCallWndProc != NULL)
{
UnhookWindowsHookEx(g_hCallWndProc);
g_hCallWndProc = NULL;
}
}

定制标题栏的入口函数为 CallWndProc(),其代码如下:
LRESULT __declspec(dllexport) CALLBACK CallWndProc(
int code, // hook code
WPARAM wParam, // undefined
LPARAM lParam // address of structure with message data (CWPSTRUCT)
)
{
DWORD dwThreadID = (DWORD)wParam;
LPCWPSTRUCT pCwpStruct = LPCWPSTRUCT(lParam);
if (HC_ACTION == code)
{
if ((pCwpStruct->message == WM_MOUSEMOVE)
|| (pCwpStruct->message == WM_SETCURSOR)
|| (pCwpStruct->message == WM_NCHITTEST)
|| (pCwpStruct->message == WM_KICKIDLE)
|| (pCwpStruct->message == WM_NCMOUSEMOVE)
|| (pCwpStruct->message == WM_MOUSEACTIVATE)
|| (pCwpStruct->message > WM_USER))
;
else
DrawFrame(pCwpStruct);
}

return CallNextHookEx(g_hCallWndProc, code, wParam, lParam);
}

上面代码中的 if 语句主要用来判断收到哪些消息时需要重绘标题栏,有兴趣的朋友可以对这段代码进行改进

在函数 DrawFrame 中将实现对窗体标题栏和边框的绘制,标题的绘制有两种方法,一是直接画图,二是贴图的方式。在本文中将实现两种方法,如果在当前目录下有 active.bmp 和 inactive.bmp 两个文件,则采用它们所代表的位图作为窗体的标题栏,否则采用画图的方式

由于只绘制标题栏,所以需要对 CallWndProc 进行过滤,对于非窗体如 Button 则不进行绘制,本文中仅以如下简单的方法来处理:
char szClassName[128] = {0};
::GetClassName(pCwpStruct->hwnd, szClassName, sizeof(szClassName));
if (strcmp(szClassName, "#32770") != 0)
return ;

实际中不能这样中,因为很多窗体的类名可能不是"#32770",比较好的方法建议去判断 pCwpStruct->hwnd 所代表的对象是否有父窗体,调用 GetParent 判断一下即可

在正式绘制之前还必须判断窗体是处于活动状态还是非活动状态,这样就可以区分在两种不同的状态下绘制不同的标题栏和边框了。

下面这段代码就是用来绘制标题栏的:

if (bActive)
hBitmap = (HBITMAP)::LoadImage(NULL, _T("active.bmp"), IMAGE_BITMAP, nWidth, nHeight, LR_LOADFROMFILE);
else
hBitmap = (HBITMAP)::LoadImage(NULL, _T("inactive.bmp"), IMAGE_BITMAP, nWidth, nHeight, LR_LOADFROMFILE);

if (NULL == hBitmap)
{
DrawTitleBar(dcWin, rcNcClient, 0);
}
else
{
dcMem = ::CreateCompatibleDC(dcWin);
hOldBitmap = (HBITMAP)::SelectObject(dcMem, hBitmap);

::StretchBlt(dcWin,
0,0,
nWidth, nHeight,
dcMem,
0,0,
nWidth, nHeight,
SRCCOPY);

::SelectObject(dcMem, hOldBitmap);
::DeleteDC(dcMem);
}


其中变量bActive为TRUE时表示窗体处于活动状态,为FALSE时表示窗体处于非活动状态。两个 LoadImage 函数分别用来将两种状态下的位图装载到内存中,以便下一步进行贴图.当 LoadImage 不成功时,表示当前目录下没有 active.bmp 和 inactive.bmp 文件中或文件格式不正确,在这处情况下就调用 DrawTitleBar 函数对标题栏进行绘画。绘画的方法可以随便,但要绘在矩形 rcNcClient 内,因为这个矩形就是标题栏所在区域。
如果 LoadImage 成功,则直接将位图贴到标题栏中。接下来就是绘制边框了,在绘制之前还需要计算出边框的所在矩形,然后再在dcWin上按要求进行绘制即可。

这种方法的关键地方是安装合适的钩子,然后对合适的消息进行处理,采用这种方法可以改变几乎任何一可见窗体的外观,包括其它程序的窗体等,对于特殊的窗体等只需要进行专门处理即可

由于编译成的是 DLL 文件中,所以可以很轻松的运用到其它程序中。只需要在需要用到的程序中调用 InstallCallWndHook 安装这个钩子即可。

5.
讨论有关XP界面的制作

by 韩举,北斗龙,china7445,黄翼等

1、如何让window9x\win2000下的标题栏增高?(能正确保证菜单、工具栏依次下将同样高度。)

设置标题栏高度,必须重载OnNcCalcSize(WM_NCCALCSIZE消息)察看MSDN可以得到下面结构
NCCALCSIZE_PARAMS *lpncsp
设置lpncsp->rgrc[0].top和lpncsp->rgrc[0].bottom就可以改变高度

2、怎样保证标题栏不露形(尤其是按钮那一块,鼠标移到非客户区时,默认按钮全部露出来了,难道要在每次WM_NCMOUSEMOVE后都重画一次标题栏?太消耗资源了吧)

不露形的原因是不显示默认按钮,不显示当然不会露出来了,
我是在CHJSkinWindow::OnInitWindow中实现的
DWORD style = GetWindowLong( m_hWnd, GWL_STYLE );

this->bsizable=style & WS_SIZEBOX; //保持状态
this->bminable=style & WS_MINIMIZEBOX; //保持状态
this->bmaxable=style & WS_MAXIMIZEBOX; //保持状态

style &= ~WS_MINIMIZEBOX; //去掉默认按钮
style &= ~WS_MAXIMIZEBOX; //去掉默认按钮
style &= ~WS_SYSMENU; //去掉默认按钮
SetWindowLong( m_hWnd, GWL_STYLE, style );

在画非客户区时必须重载WM_NCPAINT消息,在他中画,默认的去掉了,当然可以随便画了。
重画时可以设置无效区域
CDC::GetClipBox函数可以取得无效的区域,这样可以提高速度。

However,处理WM_NCCALCSIZE消息只是增加非客户区高度,对非客户区的里的菜单等位置并无改变。对于改变窗体的style,尤其是那个WS_SYSMENU,将使得你的程序在任务栏上无图标。



But,菜单栏的问题OFFICE中早就解决了,用ToolBar等模仿菜单。



对于非客户区域的计算和绘制,如果在处理WM_NCPAINT的过程中调用了DefWindowProc(),那么Windows将使用自己的解决方案:
对于非客户区域中的各种元素(窗体边框,比标题条,菜单条,滚动条)Windows都只使用内部已定义好的对应的尺寸常量
SM_CXFRAME, SM_CYFRAME,SM_CXDLGFRAME, SM_CYDLGFRAME,SM_CXBORDER,SM_CYBORDER,SM_CYMENU,SM_CXVSCROLL,SM_CYHSCROLL
这些常量通过GetSystemMetrics()获得,程序员不能通过编程改变(但是可以通过显示属性对话框中的外观选项进行设置--这是对整个系统设置而不是对某个进程)

下面试给一段窗体非客户区域的代码

void NCPaint(HWND hwnd)
{
DWORD dwStyle = GetWindowStyle();
if (dwStyle & WS_MINIMIZE || !IsWindowVisible(hwnd))
return;

// 获得HDC
HDC hdc = GetWindowDC(hwnd);

// 非客户区域
CRect rect; GetWindowRect(&rect);
rect.top = rect.left = 0; // 初始化为0
...

// 1 画边框
if (style & WS_THICKFRAME)
{
NCDrawFrame(hdc, &rect);
}
else if (style & WS_BORDER)
...

// 修改位置
InflateRect(rect, GetSystemMetrics(SM_CX...), GetSystemMetrics(SM_CY...));

// 2 画标题条
if (wStyle & WS_CAPTION)
{
NCDrawCaption(...);

// 修改位置
rect.top += GetSystemMetrics(SM_CYSIZE) + GetSystemMetrics(SM_CYBORDER);
}

// 3. 画菜单
if (有菜单)
{
NCDrawMenuBar(...);

// 修改位置
rect.top += GetSystemMetrics(SM_CYMENU);
}

// 4. 画滚动条
if (dwStyle & WS_VSCROLL)
DrawScrollBar();
if (dwStyle & WS_HSCROLL)
DrawScrollBar();

// 5. 画"size-box"
}

可见对于Windows缺省的布局程序员是能控制的,除非你不调用DefWindowProc()。但要做的工作远不止WM_NCPAINT,还用很多别的消息如WM_NCACTIVATE等。

hupflv 同志认为使用全局钩子会降低系统性能,这是对的,但是除非你不使用自画菜单
那么就很难避免(因为必须捕获下拉菜单的创建),事实上大部份都是使用窗体子类化技术
(替换窗体处理函数),系统不会太多的开销,尤其适用ATL引入的Thunk技术就没有多余的开销



将对话框资源设为无标题,编译执行后,任务栏上无图标;
但你可以通过一点处理后,可以使它有图栏,并且有最大化,最小化,大小,移动等菜单项可用。

int style=::GetWindowLong( this->GetSafeHwnd(), GWL_STYLE );
::SetWindowLong(this->GetSafeHwnd(), GWL_STYLE, style|WS_SYSMENU|WS_MAXIMIZEBOX|WS_MINIMIZEBOX);

要“大小”菜单项可用,只需将窗体的Border设为Resizing即可。因为没有标题栏,所以无从谈到默认的露型。



非客户区增加,用的便是WM_NCCALCSIZE
传入的第一个参数为FALSE时,第二个参数便为重新设置的客户区区域(通过对客户区下移,便让非客户区增加了)为TRUE的话,则需要传入一个NCCALCSIZE_PARAMS结构,一般是窗体创建时才用的。



用资源模板跟后来加风格确实有一点区别,那主要是MFC中窗口类加载资源时作了很多其它设置,而我们加风格时,只是很简单的动作。

所以要想去掉那些不良特性(闪烁,露形),那你就得动态去掉标题栏,然后在客户区开一块非客户区,模拟标题栏。但这时候,你要做其它较多事情,如对标题上各类响应。


如果拖动对话框的边框(sizing)时边框会不停的闪烁(我的系统本身是xp)。原因在于原始代码:
case WM_NCPAINT:
lReturn=oldProc(hWnd, uMsg, wParam, lParam); // 这个语句调用了系统缺省处理,画出了系统边框
s_pThis->DrawDialog(hWnd);// 再画一次覆盖了原来的边框,所以出现了闪烁
return lReturn;

我一开始想到的解决办法是不要系统缺省处理,直接画边框然后return一个值,后来trace发现这个值是变化的而且很有用,直接return一个固定的值会导致程序错误,没办法只好继续调用缺省处理。

为了解决闪烁,后来找到一个在标题栏自绘按钮的例子,发现上面有个解决办法可以解决这个问题,就是调用系统缺省处理前先使窗口不可见,调用完之后再恢复窗口可见。如下:

建议修改:
case WM_NCPAINT:
dwStyle = GetWindowLong(hWnd, GWL_STYLE);// turn off WS_VISIBLE
SetWindowLong(hWnd, GWL_STYLE, dwStyle & ~WS_VISIBLE);
lReturn=oldProc(hWnd, uMsg, wParam, lParam);// turn on WS_VISIBLE
SetWindowLong(hWnd, GWL_STYLE, dwStyle);
s_pThis->DrawDialog(hWnd);
return lReturn;



<< Home

This page is powered by Blogger. Isn't yours?