关于DirectUI中scrollbar的思考与实践1-未完待续

1. 比如我们创建的memory dc 的大小是 1000 * 1000.,而window 的 client rect 是 500 * 500.
那我们首先把 内容 画到 这个 memory dc 里去,然后根据client rect 与 memory rc 的 offset,copy 相应的
区域内容到 client rect 中。 然后定位 scroll bar的相应位置(pos)。

 
 
首先我们模拟scrollbar的操作,从简单的up 和 down 位置开始,如图:
我们的目标就是 点击 up ,模拟通用的 scrollbar up button的动作。
 
 

LRESULT CAboutDlg::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
{
    bHandled = FALSE;
    CPaintDC pdc(m_hWnd);
    CRect rcCli;
    GetClientRect(rcCli);
    
    SIZE sz;
    if ( m_bmpBK.IsNull() )
    {
        m_bmpBK.LoadBitmap(IDB_BITMAP1);
        m_bmpBK.GetSize(sz);
        g_offfset = sz.cy rcCli.Height();
    }
    CDC memDC;
    memDC.CreateCompatibleDC(pdc);
    HBITMAP hOld = memDC.SelectBitmap(m_bmpBK);
    CUIMemDC memDC1(pdc);
    BitBlt(memDC1,rcCli.left, rcCli.top, rcCli.right rcCli.left, rcCli.bottom rcCli.top,
        memDC, 0, s_top, SRCCOPY);
    memDC.SelectBitmap(hOld);
    return S_OK;
}
 
下面我们一行一行来分析这个code。
CPaintDC pdc(m_hWnd);  创建 paint 的 DC,这个是WTL的 封装,call的就是 BeginPaint
如下WTL 源代码:

CPaintDC(HWND hWnd)
    {
        ATLASSERT(::IsWindow(hWnd));
        m_hWnd = hWnd;
        m_hDC = ::BeginPaint(hWnd, &m_ps);
    }

接下来 是获得 客户区的 大小,然后是 load 一个 bitmap。
后面的代码就是把 bitmap 画到 DC 里去,并且显示出来。
BitBlt(memDC1,rcCli.left, rcCli.top, rcCli.right rcCli.left, rcCli.bottom rcCli.top,
        memDC, 0, s_top, SRCCOPY);
上面的code 我们用到了 s_top这个全局变量,在 up 和 down的时候,去改变这个 s_top
然后刷新一下 客户区,就可以实现 上下滚动的模拟了。
如下代码:

LRESULT CAboutDlg::OnDown(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
    s_top += 1;
    if ( s_top<=g_offfset)
        Invalidate();
    return S_OK;
}
LRESULT CAboutDlg::OnUp(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
    if ( s_top > 1 )
        s_top -= 1;
    if ( s_top >=0 )
        Invalidate();
    return S_OK;
}

 
这里我们会发现一个问题,如果这个 图片很大很大,这样的 scroll 的性能是很差的。
那怎么设计一个好的方法来提供性能呢?
 
我们接下来的思考是 如果把 刷新区域都控制在 客户区这一块,无论图片多大,程序只刷新这客户区就可以
了。
实现这个原来其实也很简单:因为从上面的算法中我们其实已经看到思路了。
上面的算法其实就是把一个 图 画到一个 内存DC里,然后根据客户区与这个图的大小得到滚动条
的offset,然后把需要显示的那一部分copy 到屏幕或者打印机上。把整个图画到内存里是很慢的,我们的
goal现在变成了:如何在内存里(裁剪)clip一块区域和 客户区一样大小,然后把我们需要绘制的那一部分内容copy到这块区域。
尤其对于像tree这样复杂的控件,需要一个好的算法,而且能把这个算法进行封装使之能够复用,那就是我们
软件工程师应该做的事情。
下面我们一步一步来实现它。再一次说明,上面的方法是很基础的也是最基本并且是经常用到的,下面的算法也会用到这个基本的内存dc绘制方法。
 
为了实现这个复杂 滚动条的算法,我们先来举个例子?
比如treeview 有10W 个 item,并且都是展开的,每个item算20 pixel的高度,那创建这个 memory image 是不可能的。那我们就知道了,我们要做的就是从这  10W * 20 ,找出 属于客户区的那一些items,然后只需绘制这些可见的Items。找的方法其实也很简单,让我们记住第一个可见的 item就可以了,然后就是简单的数字加减了,是不是很简单呢? NO。
 
有人提过的:

WS_EX_COMPOSITED

0x02000000L

Paints all descendants of a window in bottom-to-top painting order using double-buffering. For more information, see Remarks. This cannot be used if the window has a class style of either CS_OWNDC or CS_CLASSDC.

Windows 2000:  This flag is not supported.

 
With WS_EX_COMPOSITED set, all descendants of a window get bottom-to-top painting order using double-buffering. Bottom-to-top painting order allows a descendent window to have translucency (alpha) and transparency (color-key) effects, but only if the descendent window also has the WS_EX_TRANSPARENT bit set. Double-buffering allows the window and its descendents to be painted without flicker.
然后这篇文章 讲到了我们想要的这个方法:http://www.codeproject.com/KB/MFC/fixedbackgroundscroll.aspx
和我们上面提到的原理是一样的。
看到这里,我想你应该可以动手去实现一个 完全自己画的scrollbar,但能把这个 scrollbar 封装的很好并可以通用的,我想
还是需要一定的思考时间的,当然最后,你会发现,代码其实很简单,几乎没有几行。
 
更加优化的方法:
上面我们提到是每一次  WM_PAINT 过来的时候,我们需要刷新整个客户区。试想,当往上滚动的时候,如果我们能计算出
往上滚动了多少(其实我们是知道的),能否只刷新那一块从上面冒上来的,答案当然是: ?
看示意图:
最底下那块要往上,意味着它上面的所有部分都要往上,即意味着上面所有的部分都需要重新绘制一下。
所以答案是 :我想你已经知道了。
 
但 还是可以有 优化的地方的,这主要用在比如在一个客户区 的某个 region内进行animation。如示意图:
 
 
这就涉及到 windows 的 region,据我所知,大部分的windows开发人员都曾未想到去优化windows的redraw 过程,因为从
现在的PC配置来说,一点点刷新的性能区别根本看不出来。
 
比如: 在BeginPaint之前,可以通过GetUpdateRgn 来获得需要update的区域。
顺便说一下几个 API:

The update rectangle retrieved by the BeginPaint function is identical to that retrieved by GetUpdateRect.

BeginPaint automatically validates the update region, so any call to GetUpdateRect made immediately after the call to BeginPaint retrieves an empty update region.

The BeginPaint function automatically validates the update region, so any call to GetUpdateRgn made immediately after the call to BeginPaint retrieves an empty update region.

经我的测试,MSDN上的说法是完全正确的。你怎么call InvalidateRect,那个 GetUpdateRect 得到的就是 你想 InvalidateRect 中的 rect。

 
建立一个自己的region。
前提条件:
 
First of all goes the implementation of subtraction of a rectangle from another one.
先来实现 裁剪,从一个 rect里取得一个子rect。
1. 如果它们不相交,我们不需要做任何事情
2.
 
大部分都是 扫描线的画法。
 
 
 
参考文献:
Loki is a C++ library of designs, containing flexible implementations of common design patterns and idioms.
 
demo :
 
背景可以不滚动, 只文字滚动!或者你想怎么滚动就怎么滚动。
比上面的这个URL中的例子好多了,一点都不闪哦。
q:745122346
 
 

0
10 comments to “关于DirectUI中scrollbar的思考与实践1-未完待续”

Comments are closed.