控件树
一步一步实现自己的模拟控件(1)中的图上我们可以看到,我们的控件体系其实就是一个控件树。每一个窗口关联一个根控件,所有控件都在这个根控件之下,父控件包容并管理子控件,那么我们的Widget就应该是一个树结点。一个树结点至少有对Parent和Chilren的设置和访问接口:
void SetParent(Widget* const pNewParent);
Widget* GetParent() const;
bool InsertChild(Widget* const pChild);
bool RemoveChild(Widget* const pChild);
在父控件销毁的时候它要负责销毁其下所有的子控件(类似窗口销毁也会销毁其子窗口):
Widget::~Widget()
{
// 销毁所有子控件
WidgetSet temp(std::move(pImpl_->children_));
std::for_each(temp.begin(), temp.end(), std::mem_fn(&Widget::Destroy));
if (IsRoot())
{
// 作为根控件,同时销毁驱动
delete pImpl_->pDriver_;
}
else
{
// 不是根控件,则脱离父控件
SetParent(0);
}
delete pImpl_;
}
实现中我们使用std::set来保存子控件,这样便于防止子控件重复设置,也便于移除子控件,缺点就是不能对子控件进行排序。如果以后我们提供控件的z-order概念,那么我们就会使用能够进行排序的容器来容纳子控件。
控件区域:
windows下,我们使用RECT结构来保存控件自身相对于窗口客户区的区域,那么窗口客户区尺寸改变时也就是我们控件进行布局的时机,那我们就要在消息过滤中处理WM_SIZE消息了。
case WM_SIZE: // 让根控件适应真个客户区
{
RECT clientRect;
::GetClientRect(param.hWnd, &clientRect);
pRootWidget->SetAbsoluteRect(clientRect, false);
}
break;
我们将控件的布局交由父控件管理,也就是说我们只需要更新根控件区域便可。根控件负责对其子控件进行布局,如此递归。
控件更新:
当控件区域改变了,那么相应的其显示也应相应的进行更新,所以我们的SetAbsoluteRect接口有一个update参数用于控制是否让窗口产生无效区域激活绘制。
// 此处的update作用是控制是否立即更新显示。
// 因为模拟控件只是窗口客户区的一个区域,当区域改变时应该产生原区域和新区域or运算后区域的脏矩形
// 以使得窗口去重绘这部分区域。
// 可能有些批量性质的操作会在操作多个控件后进行整体更新,所以在对单个控件设置新区域的时候可能不会想要更新。
// 所以才加上这个是否立即更新的开关。
void SetAbsoluteRect(const RECT& rect, bool update = true);
既然提到了绘制,那么我们也应该让我们的控件展示在窗口上了。
控件绘制:
通常我们的窗口程序都是在WM_PAINT消息中进行绘制,我们的控件系统当然也需要处理此消息。
case WM_PAINT:
{
// 使用内存DC来缓冲绘制
// 目前没有计算脏矩形区域
wnd_msg_assistant::OnPaint opAssistant(param.hWnd);
pRootWidget->Draw(opAssistant.GetMemDC());
}
return S_OK;
这里引入了一个辅助对象帮助我们产生内存DC,优化我们的绘制效率。我们直接return了这个消息,也就是说我们将这个消息过滤掉了。前面WM_SIZE和WM_DESTROY我们都没有过滤,只是在这个时机对控件进行了通知或者操作。之所以要过滤WM_PAINT消息是因为外部的绘制和控件的绘制难以协调,那么我们干脆就接管了窗口客户区的绘制了。
当然,控件也需要负责绘制其子控件,那么Draw接口中便会调用子控件的Draw,如此递归使得每个控件都能够得以绘制。
首次直观的看到我们的控件:
我们在调试版本中,为每个控件生成了一个随机的颜色,根据控件的区域绘制了其边框,这样我们就第一次直观的在窗口中看到了我们的控件。
迫不及待,具有了区域的控件,我们已经急切的想要对其布局进行控制,绘制进行定制了。布局控制和绘制定制当然属于扩展部分,那么下面就将要引入我们的扩展体系了,尽请期待。