如何使用Win32 Api为窗口设置动画?

时间:2011-01-14 15:01:22

标签: winapi

你会如何定期在窗户上画画。

我想出了这个(为了清晰起见而剥离了很多)

#include <windows.h>

void DrawOntoDC (HDC dc) {

     pen    = CreatePen(...)
     penOld = SelectObject(dc, pen)

     ..... Here is the actual drawing, that
     ..... should be regurarly called, since
     ..... the drawn picture changes as time
     ..... progresses

     SelectObject(dc, pen_old);

     DeleteObject(pen);
}



LRESULT CALLBACK WindowProc(....) {
    switch(Msg) {

    case WM_PAINT: {
         PAINTSTRUCT ps;
           dc = BeginPaint(hWnd, &ps);

         .....   A Memory DC is created
         .....   In order to prevent flickering.

         HBITMAP PersistenceBitmap;
         PersistenceBitmap = CreateCompatibleBitmap(dc, windowHeight, windowHeight);

         HDC     dcMemory =  CreateCompatibleDC(dc);
         HBITMAP oldBmp = (HBITMAP) SelectObject(dcMemory, PersistenceBitmap);

         DrawOntoDC(dcMemory);

         ..... "copying" the memory dc in one go unto dhe window dc:

         BitBlt ( dc, 
                  0, 0, windowWidth, windowHeight,
                  dcMemory,
                  0, 0,
                  SRCCOPY
                );

         ..... destroy the allocated bitmap and memory DC
         ..... I have the feeling that this could be implemented
         ..... better, i.e. without allocating and destroying the memroy dc
         ..... and bitmap with each WM_PAINT.

         SelectObject(dcMemory, oldBmp);
         DeleteDC(dcMemory);
         DeleteObject(PersistenceBitmap);

     EndPaint  (hWnd, &ps);
         return 0;
    }
    default:
        return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
}


DWORD WINAPI Timer(LPVOID p) {

  ..... The 'thread' that makes sure that the window
  ..... is regularly painted.

  HWND hWnd = (HWND) *((HWND*) p);

  while (1) {
     Sleep(1000/framesPerSecond);
     InvalidateRect(hWnd, 0, TRUE);
  }
}


int APIENTRY WinMain(...) {

    WNDCLASSEX windowClass;
       windowClass.lpfnWndProc         = WindowProc;
       windowClass.lpszClassName       = className;
       ....

    RegisterClassEx(&windowClass);

    HWND hwnd = CreateWindowEx(
                ....
                 className,
                 ....);


    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);

    DWORD threadId;
    HANDLE hTimer  = CreateThread(
      0, 0,
      Timer,
     (LPVOID) &hwnd,
     0, &threadId );

    while( GetMessage(&Msg, NULL, 0, 0) ) {
       ....
    }

    return Msg.wParam;
}

我想有很多东西可以改进,我很欣赏任何指向我忽略的东西。

2 个答案:

答案 0 :(得分:4)

使用工作线程执行此类操作并不是最佳选择。 鉴于绘画的最佳代码路径始终是通过WM_PAINT,留下两种方法:

  1. 只需在GUI线程上创建一个计时器,将WM_TIMER消息直接发送到timerproc或窗口,然后调用引擎的OnTick()部分。如果任何精灵移动,它们使用InvalidateRect()使其区域无效,并且窗口通过自动发布WM_PAINT来跟进。如果游戏相对空闲,这具有CPU使用率非常低的优势。

  2. 大多数游戏都希望使用基于低优先级WM_TIMER的计时器实现更严格的计时。在这种情况下,您实现了类似这样的游戏循环:

  3. 消息循环:

    while(stillRunning)
    {
      DWORD ret = MsgWaitForMultipleObjects(0,NULL,FALSE,frameIntervalMs,QS_ALLEVENTS);
      if(ret == WAIT_OBJECT_0){
        while(PeekMessage(&msg,0,0,0,PM_REMOVE)){
          TranslateMessage(&msg);
          DispatchMessage(&msg);
      }
      if(TickGame()) // if TickGame indicates that enough time passed for stuff to change
        RedrawWindow(hwndGame,...); // Dispatch a WM_PAINT immediately.
    }
    

    这种消息循环的危险在于,如果...应用程序进入任何一种模态状态: - 用户开始拖动窗口/弹出一个模态对话框,然后消息被泵送模态循环,所以动画停止。因此,如果需要将高性能消息循环与模态操作混合使用,则需要使用回退计时器。


    WRT你的WM_PAINT实现 - 通常更好地(重新)创建你的backbuffer以响应WM_SIZE消息。这样它的大小总是正确的,并且你不会产生每秒多次重建大型内存缓冲区的相当大的成本。

答案 1 :(得分:0)

从各个地方借来一点点,对于类似的问题,我想出了以下方法。以下是测试该概念的测试应用程序。

在此测试应用程序中,我使用的是MFC静态窗口,并通过使用MFC静态窗口的句柄定期调用::SetWindowText()函数来更新文本字符串。这样可以很好地显示行进的直角括号来演示动画的效果。

将来,我打算使用驻留在内存中的位图图像,该图像在动画循环中进行了修改,然后发布到附加到静态文本窗口的位图中。这项技术可以使动画位图呈现出正在进行中的事物的更优美的显示。

使用MFC对话框应用程序测试了此概念,该应用程序包含用于进度指示器的两个静态窗口以及用于启动和停止进度指示器的两个附加按钮“开始”和“停止”。目的是当按下“开始”按钮时,在静态窗口上写入一系列大于号的符号,然后清除它们,然后再次启动。因此,动画看起来像是一个LED标志,其中的箭头在水平字幕中从左向右行进。

这两个按钮只不过将动画对象中的指示器设置为一个(打开)或零(关闭)而已。进行实际动画处理的动画对象线程只是从m_state变量中读取而不会对其进行修改。

出于此测试目的,对计时器延迟量进行了硬编码。可以很容易地成为一个参数。

对话框仍然响应,即使正在更新,我也可以显示对话框应用程序的默认“关于盒子”,然后左右移动“关于盒子”。我还可以将对话框应用程序本身拖动到屏幕周围(不显示“关于对话框”,因为它是模式对话框),而静态窗口仍在更新。

screen shot of the dialog application showing the About Box displayed and the updating static window

动画类源

动画逻辑的源代码是一个简单的类,它启动一个线程,然后该线程更新指定的对话框控件。动画循环是该类的静态方法,但循环使用的数据是在对象本身中指定的,因此可以使用同一静态循环对不同的对象执行多个动画。

这种方法非常简单明了。它没有将更复杂的方法与线程池和计时器池一起使用,而是将线程和计时器对象专用于单个动画。显然,这种方法无法很好地扩展,但是对于带有几个动画的应用程序,它的效果很好。

class AnimatedImage
{
    UINT     m_state;          // current on/off state of the animation. if off (0) then the window is not updated
    UINT     m_itemId;         // control identifier of the window that we are going to be updating.
    HWND     m_targetHwnd;     // window handle of the parent dialog of the window we are going to be updating
    UINT     m_i;              // position for the next right angle bracket
    wchar_t  m_buffer[32];     // text buffer containing zero or more angle brackets which we use to update the window
    DWORD    m_lastError;      // result of GetLastError() in case of an error.
    HANDLE   m_hTimer;         // handle for the timer
    HANDLE   m_hThread;        // handle for the thread created.
    LARGE_INTEGER m_liDueTime; // time delay between updates

public:
    AnimatedImage(UINT itemId = 0, HWND hWnd = NULL) : m_state(0), m_itemId(itemId), m_targetHwnd(hWnd), m_i(0), m_lastError(0), m_hTimer(NULL), m_hThread(NULL) { memset(m_buffer, 0, sizeof(m_buffer))             ; }
    ~AnimatedImage() { Kill();  CloseHandle(m_hTimer);  CloseHandle(m_hThread); }    // clean up the timer and thread handle.

    static unsigned __stdcall loop(AnimatedImage *p);    // animation processing loop
    void Run();        // starts the animation thread
    void Start();      // starts the animation
    void Stop();       // stops the animation
    void Kill();       // indicates the thread is to exit.

    // Functions used to get the target animation window handle
    // and to set the parent window handle and the dialog control identifier.
    // This could be simpler by just requiring the target animation window handle
    // and letting the user do the GetDlgItem() function themselves.
    // That approach would make this class more flexible.
    HWND GetImageWindow() { return ::GetDlgItem(m_targetHwnd, m_itemId); }
    void SetImageWindow(UINT itemId, HWND hWnd) { m_itemId = itemId; m_targetHwnd = hWnd; }
};

unsigned __stdcall AnimatedImage::loop(AnimatedImage *p)
{
    p->m_liDueTime.QuadPart = -10000000LL;

    // Create an unnamed waitable timer. We use this approach because
    // it makes for a more dependable timing source than if we used WM_TIMER
    // or other messages. The timer resolution is much better where as with
    // WM_TIMER is may be no better than 50 milliseconds and the reliability
    // of getting the messages regularly can vary since WM_TIMER are lower
    // in priority than other messages such as mouse messages.
    p->m_hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
    if (NULL == p->m_hTimer)
    {
        return 1;
    }

    for (; ; )
    {
        // Set a timer to wait for the specified time period.
        if (!SetWaitableTimer(p->m_hTimer, &p->m_liDueTime, 0, NULL, NULL, 0))
        {
            p->m_lastError = GetLastError();
            return 2;
        }

        // Wait for the timer.
        if (WaitForSingleObject(p->m_hTimer, INFINITE) != WAIT_OBJECT_0) {
            p->m_lastError = GetLastError();
            return 3;
        }
        else {
            if (p->m_state < 1) {
                p->m_i = 0;
                memset(p->m_buffer, 0, sizeof(m_buffer));
                ::SetWindowText(p->GetImageWindow(), p->m_buffer);
            }
            else if (p->m_state < 2) {
                // if we are updating the window then lets add another angle bracket
                // to our text buffer and use SetWindowText() to put it into the
                // window area.
                p->m_buffer[p->m_i++] = L'>';

                ::SetWindowText(p->GetImageWindow(), p->m_buffer);
                p->m_i %= 6;      // for this demo just do a max of 6 brackets before we reset.
                if (p->m_i == 0) {
                    // lets reset our buffer so that the next time through we will start
                    // over in position zero (first position) with our angle bracket.
                    memset(p->m_buffer, 0, sizeof(m_buffer));
                }
            }
            else {
                // we need to exit our thread so break from the loop and return.
                break;
            }
        }
    }

    return 0;
}
void AnimatedImage::Run()
{
    m_hThread = (HANDLE)_beginthreadex(NULL, 0, (_beginthreadex_proc_type)&AnimatedImage::loop, this, 0, NULL);
}

void AnimatedImage::Start()
{
    m_state = 1;
}

void AnimatedImage::Stop()
{
    m_state = 0;
}

void AnimatedImage::Kill()
{
m_state = 3;
}

如何使用课程

对于这个简单的测试对话框应用程序,我们只为两个动画创建了几个全局对象。

AnimatedImage xxx;
AnimatedImage xx2;

在对话框应用程序的OnInitDialog()方法中,动画在返回之前被初始化。

// TODO: Add extra initialization here

xxx.SetImageWindow(IDC_IMAGE1, this->m_hWnd);
xxx.Run();
xx2.SetImageWindow(IDC_IMAGE2, this->m_hWnd);
xx2.Run();
return TRUE;  // return TRUE  unless you set the focus to a control

有两个按钮单击处理程序,用于处理对“开始”按钮或“停止”按钮的单击。

void CMFCApplication2Dlg::OnBnClickedButton1()
{
    // TODO: Add your control notification handler code here

    xxx.Start();
    xx2.Start();
}


void CMFCApplication2Dlg::OnBnClickedButton2()
{
    // TODO: Add your control notification handler code here

    xxx.Stop();
    xx2.Stop();
}

对话框应用程序的主对话框资源在资源文件中定义如下。

IDD_MFCAPPLICATION2_DIALOG DIALOGEX 0, 0, 320, 200
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_APPWINDOW
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,209,179,50,14
    PUSHBUTTON      "Cancel",IDCANCEL,263,179,50,14
    CTEXT           "TODO: Place dialog controls here.",IDC_STATIC,10,96,300,8
    LTEXT           "Static",IDC_IMAGE1,7,7,110,21
    LTEXT           "Static",IDC_IMAGE2,64,43,112,27
    PUSHBUTTON      "Start",IDC_BUTTON1,252,16,50,19
    PUSHBUTTON      "Stop",IDC_BUTTON2,248,50,57,21
END