Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1218002
  • 博文数量: 950
  • 博客积分: 10000
  • 博客等级: 上将
  • 技术积分: 13070
  • 用 户 组: 普通用户
  • 注册时间: 2008-08-04 09:23
文章分类

全部博文(950)

文章存档

2011年(1)

2008年(949)

我的朋友

分类: C/C++

2008-08-04 09:37:03

下载本文示例代码

原文链接

下载源代码

介绍

从零开发自定义控件常常是不需要的,因为标准控件组是相当全面的,如果不够用,子类化或自绘等方法就可以搞掂这个工作。这是一个不应被忽略的要点。在从零开发一个自定义控件时,千辛万苦获得的控件往往会不如标准(控件)。
那就是说,这里只有少数真地缺少的控件,如果我们想要在我们的应用程序中部署它们,除了无中生有地构建它们别无他法。有一个这样的情况就是名称为“层叠式窗体控件”,或无论什么它的其他称呼,例如:Spybot或Outlook。因为它不在标准控件之中并且因为它是一个有趣的练习,本指南讲解了如何开发这类控件,并一步一步地给予讲解。
本指南的目标读者为程序员新手,在开始之前,我想挑战你一下:即在阅读本文的情况下先尝试自己开发这个控件。尽管这看起来可能会让人退缩或你可能不知道从哪里开始,它不是像你想像得那样难。尝试一下,看看你能走多远,这时再回来看看本指南并检验一下我所说的话。提示:它完全与窗体的重新恢复尺寸和重新复位有关,没有其他。

我们要完成什么

目标是一个“层叠式窗体控件”。就是它。它将会被尽可能地泛型化并会阐明如何聚集该类控件的一个。
热心的读者可能希望知道我在写这个演示工程时写了这个指南。下面的指导、解释和代码实际上就是在上面的截屏中的层叠窗体控件(准确地说来就是图中左边那个控件)的开发。
让我们从代码开始。

过程详解

工程开始

创建工作是简单的。创建一个新的基于对话框的工程,并设置警告级别为4(工程设置,C/C 标签)。级别4将确保任何可疑事物给我们带来注意以使得由我们来决定要做什么“这里提示的警告在绝大多数情况下可以被安全地忽略”(此语出自文档)
让我们在该控件上开始工作。创建一个用CStatic作为基类的新的MFC类命名为CStackedWndCtrl。

在资源编辑器中,添加一个图片控件ID号为IDC_SWC。保留Type的值为缺省的Frame并将Color置为Black。

使用MFC ClassWizard添加一个数据变量到IDC_SWC命名为m_StackedWndCtrl,确保选择了Control作为Category以及CStackedWndCtrl作为变量类型。

在OK上点击,弹出一个消息框提示我们确保我们已经为类CStackedWndCtrl包含头文件在我们的对话框代码中。如果你没有包含它现在就要做了。

数据结构

任何控件的主要部分就是一个数据结构,数据结构可以保持将要显示的信息。
好的,什么将会被显示?该控件用面板制作出来,每个面板包含两个窗体,一个标题窗体和一个内容窗体。下面的图片说明了这个概念。

控件的机制要求只有一个面板的内容窗体在一个时间内显示。在一个面板上点击标题窗体将触发其相应的内容窗体显示,并且也隐藏了当前显示面板的内容窗体。
因此,数据结构将包含一对指向CWnd 对象的指针和一个布尔标识值以指出是否显示或隐藏这个面板的内容窗体。不需要任何其他的东西了。

#include 

class CStackedWndCtrl : public CStatic
{ .... ....

// Attributes
protected:

typedef struct
{
CWnd* m_pwndRubric;
CWnd* m_pwndContent;
BOOL m_bOpen;
} TDS_PANE, *PTDS_PANE;

CArray m_arrPanes;

....
....
}

对于这些结构的保存、检索和操作,用一个数组是一个方便的且足够的方法。记住为了使用这个数组模版,我们需要包含相应的头文件。
下一个任务是写一个允许我们添加面板到控件上的public方法。这没有什么困难。我们使窗体对象的指针作为参数传递,并设置新的面板如其所显示的一样。

int CStackedWndCtrl::AddPane( CWnd* pwndRubric, CWnd* pwndContent )
{
// 隐藏无论哪一个正在显示面板的内容窗体
//我们将总是显示最近添加的面板的内容窗体
for( int i = 0; i < m_arrPanes.GetSize(); i )
if( m_arrPanes[ i ]->m_bOpen )
m_arrPanes[ i ]->m_bOpen = FALSE; //创建一个新的面板结构
PTDS_PANE pPane = new TDS_PANE; if( pPane == NULL )
{
AfxMessageBox( "Failed to add a new pane to"
" the stack.\n\nOut of memory." );
return -1;
} // 拷贝指针到标题和内容窗体
//同时,设置这个面板为打开状态 pPane->m_pwndRubric = pwndRubric;
pPane->m_pwndContent = pwndContent;
pPane->m_bOpen = TRUE; // 添加该新面板到栈的尾部
int iIndex = m_arrPanes.Add( pPane ); // 重新排列栈
RearrangeStack(); // 返回新面板的索引号
return iIndex;
}

在我们担心排列和显示面板之前(如果你想要测试这个代码,只要参考RearrangeStack
方法的调用 ),我们要确保在退出时该结构体被完全删除是非常重要的,以免内存泄漏。我们在析构器中执行该任务,如下所示:
CStackedWndCtrl::~CStackedWndCtrl()
{ for( int i = 0; i < m_arrPanes.GetSize(); i )
{
//删除标题窗体
m_arrPanes[ i ]->m_pwndRubric->DestroyWindow();

delete m_arrPanes[ i ]->m_pwndRubric; // 删除内容窗体
m_arrPanes[ i ]->m_pwndContent->DestroyWindow(); delete m_arrPanes[ i ]->m_pwndContent; // 删除结构体
delete m_arrPanes[ i ];
} m_arrPanes.RemoveAll();
}

简单填充。我们遍历该面板上的数组,销毁每个窗体,然后删除每个窗体对象,然后删除每个面板对象,并且最后,从数组中移除所有指针。
这个功能足以使得CStackedWndCtrl类可以做它的工作。我们可以添加面板,同时它们(译注:指面板)在控件被销毁时被适当释放。

可视的魔力

None of it, 我想.排列和显示控件的算法是相当简单的。
我们遍历面板,通过一个预先估量消除顶部框架,m_iRubricHeight,它在演示程序中被设置为一个默认的值(可以自由测试)当我们点击打开的面板,我们用余下来要显示的标题窗体的数量来计算该面板的内容窗体的尺寸。请看下面的代码。

void CStackedWndCtrl::RearrangeStack()
{ CRect rFrame; GetClientRect( &rFrame ); for( int i = 0; i < m_arrPanes.GetSize(); i )
{
// 标题窗体总是显示
m_arrPanes[ i ]->m_pwndRubric->SetWindowPos( NULL, 0, rFrame.top, rFrame.Width(), m_iRubricHeight, SWP_NOZORDER | SWP_SHOWWINDOW ); // 只有已标记面板的内容窗体被显示
// 如果它们没有准备好,所有其他的都隐藏 if( m_arrPanes[ i ]->m_bOpen )
{
// 从框架的底部,去掉余下要显示的那些标题窗体的一样高度的尺寸 int iContentWndBottom = rFrame.bottom -
( ( m_arrPanes.GetSize() - i ) * m_iRubricHeight ); m_arrPanes[ i ]->m_pwndContent->SetWindowPos(
NULL, 0,
rFrame.top m_iRubricHeight,
rFrame.Width(),
iContentWndBottom - rFrame.top,
SWP_NOZORDER | SWP_SHOWWINDOW ); //下一个标题窗体将被放置于该面板内容窗体的正下方 rFrame.top = iContentWndBottom;
}
else
m_arrPanes[ i ]->m_pwndContent->ShowWindow( SW_HIDE ); //框架的顶部偏移一个标题窗体的高度 rFrame.top = m_iRubricHeight;
}
}


以上处理了控件的排列和显示。
让我们现在添加一个调用到PreSubclassWindow以去除图片控件周围的黑框。在资源编辑器工作时这是有效的,在应用程序运行时它是不必的且难看。
void CStackedWndCtrl::PreSubclassWindow() 
{ // 移除黑框并夹住子控件以避免闪烁

ModifyStyle( SS_BLACKFRAME, WS_CLIPCHILDREN ); CStatic::PreSubclassWindow();
}
我们已经获得机会添加WS_CLIPCHILDREN 标志以在重新改变控件尺寸时减少闪烁,这提醒我…
…确保该控件能在需要时改变自己的尺寸大小总是一个好主意。在此情况中,该功能是相当容易实现的。调出Classwizard,为WM_SIZE添加一个消息句柄,并做一个调用到RearrangeStack。
void CStackedWndCtrl::OnSize(UINT nType, int cx, int cy) 
{
CStatic::OnSize(nType, cx, cy); RearrangeStack();
}

我们几乎已经做好了。如果你添加一些测试面板,编译并运行;这个层叠式控件将显示所有标题窗体和最后的面板的内容窗体。
当然,这个控件不会对用户点击标题窗体做出反应。我们还没有为其写响应代码啊。它是我们任务清单上的下一个也是最后一个任务了。

标题窗体的惟一需求

至于我们的控件,标题和内容窗体可以是任何一种窗体。照字面意思,可以是对话框、static控件、列表框/控件、树控件、日历控件、编辑框/ richedit控件、generic窗体、甚至自定义控件。如果我们可以获得一个指向它的CWnd指针,CStackedWndCtrl类会如预期一样工作。这里惟一的限制是常识,而不是一个技术问题。举个例子,一个组合框可能被设置为标题或内容窗体,但是其适宜性相当值得怀疑。
然而,这里有一个必要条件,同时它被应用于标题窗体。当它被点击,它必须通知其父(一个CStackedWndCtrl 对象)以使得相关内容窗体可以被显示。我们将通过发送一个消息完成这个任务。
为了简化,我将用按钮作为标题窗体。它们毕竟是绝大多数可能的选择。我们将从CButton继承一个类,并且添加这个有点特别的功能。
那么,我们现在创建一个继承于CButton的名为CTelltaleButton的类。添加下面的消息定义到它的头文件,和一个BN_CLICKED(反射消息)的消息处理程序。

// In TelltaleButton.h

#define WM_RUBRIC_WND_CLICKED_ON ( WM_APP 04100 )

// In TelltaleButton.cpp

void CTelltaleButton::OnClicked()
{ GetParent()->SendMessage( WM_BUTTON_CLICKED, (WPARAM)this->m_hWnd );
}

标题窗体将发送一个包含其自己句柄的消息,如wParam,有了这个信息,它的父控件将可以了解到哪一个标题窗体已经被点击了。
现在,我们通过手工添加一个方法到其消息映射在CStackedWndCtrl中处理这个消息如下:

// In StackedWndCtrl.h

#define WM_RUBRIC_WND_CLICKED_ON ( WM_APP 04100 ) ...
... // 生成消息映射函数

protected:
//{{AFX_MSG(CStackedWndCtrl)
afx_msg void OnSize(UINT nType, int cx, int cy);
//}}AFX_MSG
afx_msg LRESULT OnRubricWndClicked(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP() // In StackedWndCtrl.cpp

...
...

BEGIN_MESSAGE_MAP(CStackedWndCtrl, CStatic)
//{{AFX_MSG_MAP(CStackedWndCtrl)
ON_WM_SIZE()
//}}AFX_MSG_MAP
ON_MESSAGE(WM_RUBRIC_WND_CLICKED_ON, OnRubricWndClicked)
END_MESSAGE_MAP() ...
... LRESULT CStackedWndCtrl::OnRubricWndClicked(WPARAM wParam, LPARAM /*lParam*/)
{
HWND hwndRubric = (HWND)wParam;
BOOL bRearrange = FALSE; for( int i = 0; i < m_arrPanes.GetSize(); i )
if( m_arrPanes[ i ]->m_pwndRubric->m_hWnd == hwndRubric )
{
// 只有除了属于当前已打开面板的一个标题窗体被点击时才重新排列控件 if( m_arrPanes[ i ]->m_bOpen == FALSE )
{
m_arrPanes[ i ]->m_bOpen = TRUE;
bRearrange = TRUE;
}
}
else
m_arrPanes[ i ]->m_bOpen = FALSE; if( bRearrange )
RearrangeStack(); // 如果已发送消息的标题窗体希望知道是否控件已被重新排列,返回标志 return bRearrange;
}

它完全归结为遍历面板以寻找已被点击的标题窗体。如果它不同于当前打开的面板的那个(标题窗体),就重新排列控件。

一些Eye Candy

因为对于它的标题和内容窗体所可以被使用的控件来说CStackedWndCtrl是非常灵活,这就很容易使其样式活泼起来。为了演示如何做到这样,我已经在演示工程中包含了一个"普通"控件和一个Davide Calabro的阴影按钮及Everaldo Coelho的图标的控件。正如你能看到的,通过检查演示工程中的代码,没有一行在CStackedWndCtrl中的代码需要被修改。正如其所应该的那样。
我们的短暂的旅程就要结束了,我的朋友;我们从此会各走各路了。我希望你已经用我已向你展示的东西来播下你想像力的种子,而且我们的quiet dealings会对你有益。

反馈

我的意图是提供一个编码清楚的指南,它理解和学习起来务求尽可能的简单。我确信会有比我这里这个功能的实现更好的解决方案。任何关于改进、简单化或更好解释代码的建议我都欢迎。

感谢

为此演示工程,我已用到一个CResizableDialog by Paolo Messina的一个老的版本,我已经喜欢为Code Project写文章。感谢Paolo。
另一个意大利人的工作,Davide Calabro的吸引人的CButtonST,已被用于演示工程中。谢谢Davide。我已在演示工程中使用了一些Everaldo Coelho的图标。你可以找到更多他的工作here 和 here。谢谢Everaldo。
我也使用了Dan Moulding的Visual Leak Detector来检查内存 。这是我向所有人推荐的一个非常、非常好用的工具。感谢Dan。
最后,我想要向每个分享或使之尽可能容易被自由分享的人表示我的感谢。我看到大家撰写文章、指南,在论坛里帮助陌生人,而我多次被他们的慷慨所感动并感到自惭形秽。(对我来说)回馈一些给大家是太令我高兴了。

下载本文示例代码
阅读(382) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~