Стратегия создания многослойного окна с дочерними окнами (элементы управления)

Я хочу создать окно неправильной формы / со скином (пока только с закругленными углами, смешанными с альфа-каналом). После создания окна верхнего уровня я обрабатываю сообщение WM_CREATE и делаю следующее:

  1. Создать совместимую память DC
  2. Создайте раздел DIB, совместимый с окном DC
  3. Выберите DIB в память DC
  4. Сделать рисунок моего фона
  5. Применить альфа-канал и предварительно умножить значения RGB
  6. Вызовите UpdateLayeredWindow()

Позже я планирую округлить края, установив альфа-канал и предварительно умножив рассматриваемые биты, чтобы это произошло.

Теперь, если я создам, например, кнопку в моем окне, она не будет отображаться сама. Я знаю почему, и я пытаюсь найти элегантное решение для создания своих пользовательских элементов управления (дочерних окон) здесь.

Мой первоначальный подход состоял в том, чтобы сначала отказаться от использования дочерних окон и просто позволить окну верхнего уровня выполнять всю прорисовку, а также обработку ввода, тестирование попаданий и так далее. Я обнаружил, что это утомительно, и вместо этого я хочу позволить Windows справиться со всем этим для меня.

Теперь я знаю, что если я создаю дочернее окно, оно, конечно, ведет себя нормально (например, реагирует на ввод пользователя), и я хочу использовать это. Я планирую создать дочерние окна (пользовательские элементы управления), как правило, используя CreateWindowEx(), чтобы они получали дескриптор окна и получали оконные сообщения без необходимости беспокоиться о передаче их вручную.

Каким-то образом мне нужно покрасить эти окна, и, как я вижу, единственный возможный способ сделать это - из рутины, которая рисует все окно верхнего уровня. Мне нужно придумать какую-то логику, чтобы заставить процедуру рисования окна верхнего уровня рисовать мои дочерние окна, когда это необходимо. Насколько я понимаю, функция UpdateLayeredWindow() должна перерисовывать все окно.

Имеет ли смысл, например, чтобы дочерние элементы управления отображали свое изображение, которое отправляется в процедуру рисования окна верхнего уровня? Как, например, дочернее окно отправляет пользовательский WM в окно верхнего уровня, передавая указатель на его отображаемое растровое изображение в виде WPARAM и указатель на структуру, определяющую его положение и размер в качестве LPARAM.

Есть идеи получше? Имеет ли это какой-либо смысл вообще?

Спасибо Эйрик

3 ответа

Я пытался сделать очень похожую вещь. Из чтения этой и другой поисковой сети. Он использует рекомендованный механизм для рисования CWnd (или HWND), и его дочерние элементы на вашем собственном CDC (или HDC) должны использовать API печати.

CWnd имеет методы Print и PrintClient, которые правильно отправляют WM_PRINT. Есть также методы Win32: PrintWindow.

Сначала мне было трудно заставить это работать, но я в итоге получил правильный метод и параметры. Код, который работал для меня, был:

void Vg2pImageHeaderRibbon::Update() {
    // Get dimensions
    CRect window_rect;
    GetWindowRect(&window_rect);

    // Make mem DC + mem  bitmap
    CDC* screen_dc = GetDC(); // Get DC for the hwnd
    CDC dc;
    dc.CreateCompatibleDC(screen_dc);
    CBitmap dc_buffer;
    dc_buffer.CreateCompatibleBitmap(screen_dc, window_rect.Width(), window_rect.Height());
    auto hBmpOld = dc.SelectObject(dc_buffer);

    // Create a buffer for manipulating the raw bitmap pixels (per-pixel alpha).
    // Used by ClearBackgroundAndPrepForPerPixelTransparency and CorrectPerPixelAlpha.
    BITMAP raw_bitmap;
    dc_buffer.GetBitmap(&raw_bitmap);
    int bytes =  raw_bitmap.bmWidthBytes * raw_bitmap.bmHeight;
    std::unique_ptr<char> bits(new char[bytes]);

    // Clears the background black (I want semi-transparent black background).
    ClearBackgroundAndPrepForPerPixelTransparency(dc, raw_bitmap, bytes, bits.get(), dc_buffer);

    // To get the window and it's children to draw using print command
    Print(&dc, PRF_CLIENT | PRF_CHILDREN | PRF_OWNED);

    CorrectPerPixelAlpha(dc, raw_bitmap, bytes, bits.get(), dc_buffer);

    // Call UpdateLayeredWindow
    BLENDFUNCTION blend = {0};
    blend.BlendOp = AC_SRC_OVER;
    blend.SourceConstantAlpha = 255;
    blend.AlphaFormat = AC_SRC_ALPHA;
    CPoint ptSrc;
    UpdateLayeredWindow(
        screen_dc, 
        &window_rect.TopLeft(), 
        &window_rect.Size(), 
        &dc, 
        &ptSrc, 
        0, 
        &blend, 
        ULW_ALPHA
    );

    SelectObject(dc, hBmpOld);
    DeleteObject(dc_buffer);
    ReleaseDC(screen_dc);
}

Это сработало для меня, как есть. Но если ваше окно или дети не поддерживают WM_PRINT, я посмотрел, как это было реализовано для класса CView. Я обнаружил, что этот класс предоставляет виртуальный метод OnDraw(CDC* dc), который снабжен DC для рисования. WM_PAINT реализован примерно так:

CPaintDC dc(this);
OnDraw(&dc);

И WM_PAINT реализован:

CDC* dc = CDC::FromHandle((HDC)wParam);
OnDraw(dc);

Таким образом, WM_PAINT и WM_PRINT приводят к OnDraw(), и код рисования реализуется один раз.

Вы можете добавить эту же логику в свой собственный класс, производный от CWnd. Это может быть невозможно при использовании волшебников классов visual studio. Мне пришлось добавить следующее в блок карты сообщений:

BEGIN_MESSAGE_MAP(MyButton, CButton)
    ...other messages
    ON_MESSAGE(WM_PRINT, OnPrint)
END_MESSAGE_MAP()

И мой обработчик:

LRESULT MyButton::OnPrint(WPARAM wParam, LPARAM lParam) {
    CDC* dc = CDC::FromHandle((HDC)wParam);
    OnDraw(dc);
    return 0;
}

ПРИМЕЧАНИЕ. Если вы добавите пользовательский обработчик WM_PRINT в класс, который уже поддерживает это автоматически, вы потеряете реализацию по умолчанию. Для OnPrint нет метода CWnd, поэтому вы должны использовать метод Default() для вызова обработчика по умолчанию.

Я не пробовал следующее, но я ожидаю, что это работает:

LRESULT MyCWnd::OnPrint(WPARAM wParam, LPARAM lParam) {
    CDC* dc = CDC::FromHandle((HDC)wParam);
    // Do my own drawing using custom drawing
    OnDraw(dc);

    // And get the default handler to draw children
    return Default();
}

Выше я определил несколько странных методов: ClearBackgroundAndPrepForPerPixelTransparency и CorrectPerPixelAlpha. Это позволяет мне установить фон моего диалогового окна полупрозрачным, когда дочерние элементы управления будут полностью непрозрачными (это моя прозрачность для каждого пикселя).

// This method is not very efficient but the CBitmap class doens't
// give me a choice I have to copy all the pixel data out, process it and set it back again.
// For performance I recommend using another bitmap class
//
// This method makes every pixel have an opacity of 255 (fully opaque).
void Vg2pImageHeaderRibbon::ClearBackgroundAndPrepForPerPixelTransparency( 
    CDC& dc, const BITMAP& raw_bitmap, int bytes, char* bits, CBitmap& dc_buffer 
) {
    CRect rect;
    GetClientRect(&rect);
    dc.FillSolidRect(0, 0, rect.Width(), rect.Height(), RGB(0,0,0));
    dc_buffer.GetBitmapBits(bytes, bits);
    UINT* pixels = reinterpret_cast<UINT*>(bits);
    for (int c = 0; c < raw_bitmap.bmWidth * raw_bitmap.bmHeight; c++ ){
        pixels[c] |= 0xff000000;
    }
    dc_buffer.SetBitmapBits(bytes, bits);
}

// This method is not very efficient but the CBitmap class doens't
// give me a choice I have to copy all the pixel data out, process it and set it back again.
// For performance I recommend using another bitmap class
//
// This method modifies the opacity value because we know GDI drawing always sets
// the opacity to 0 we find all pixels that have been drawn on since we called 
// For performance I recommend using another bitmap class such as the IcfMfcRasterImage
// ClearBackgroundAndPrepForPerPixelTransparency. Anything that has been drawn on will get an 
// opacity of 255 and all untouched pixels will get an opacity of 100.
void Vg2pImageHeaderRibbon::CorrectPerPixelAlpha( 
    CDC& dc, const BITMAP& raw_bitmap, int bytes, char* bits, CBitmap& dc_buffer 
) {
    const unsigned char AlphaForBackground = 100; // (0 - 255)
    const int AlphaForBackgroundWord = AlphaForBackground << 24;
    dc_buffer.GetBitmapBits(bytes, bits);
    UINT* pixels = reinterpret_cast<UINT*>(bits);
    for (int c = 0; c < raw_bitmap.bmWidth * raw_bitmap.bmHeight; c++ ){
        if ((pixels[c] & 0xff000000) == 0) {
            pixels[c] |= 0xff000000;
        } else {
            pixels[c] = (pixels[c] & 0x00ffffff) | AlphaForBackgroundWord;
        }
    }
    dc_buffer.SetBitmapBits(bytes, bits);
}

Вот снимок экрана моего тестового приложения. Когда пользователь наводит указатель мыши на кнопку "дополнительные кнопки", создается диалоговое окно с полупрозрачным фоном. Кнопки с "B1" по "B16" являются дочерними элементами управления, производными от CButton и рисуются с помощью показа вызова Print() выше. Вы можете видеть полупрозрачный фон у правого края экрана и между кнопками.

Я думаю, что я собираюсь пойти на это решение:

Окно верхнего уровня

Окно верхнего уровня поддерживает два растровых изображения. Один, который является отображаемым окном, и один без каких-либо дочерних элементов управления. Последний нужно будет перерисовать только тогда, когда окно меняет размер. В окне будет обработчик сообщений, который отображает дочерний элемент управления на отображаемом растровом изображении. Обработчик сообщения будет ожидать указатель либо на DIB, содержащий дочерний элемент управления, либо на фактические биты (не уверенные, которые являются лучшими в данный момент), как WPARAM, и указатель на структуру, содержащую прямоугольник, которым должен быть дочерний элемент. втягивается как LPARAM. Будет выполнен вызов BitBlt(), чтобы очистить нижележащую поверхность (это то место, куда входит другое растровое изображение) до вызова AlphaBlend() для рендеринга растрового изображения дочернего элемента управления на отображаемую растровое растровое изображение.

Родительское окно будет вызывать EnumChildWindows всякий раз, когда оно изменяется или по какой-то причине необходимо перерисовать его дочерние элементы. Конечно, здесь может быть установлен какой-то режим аннулирования, чтобы уменьшить ненужную визуализацию дочерних элементов управления. Однако не уверен, стоит ли увеличивать скорость.

Детские окна

При создании экземпляра дочернего элемента управления создается внутренняя битовая карта, совместимая с битовой картой окна верхнего уровня. Дочерний рендерит себя в это внутреннее растровое изображение, и всякий раз, когда ему нужно перерисовать, он уведомляет свое родительское окно с помощью функции SendMessage(), передавая указатель на свое растровое изображение как WPARAM, и RECT как LPARAM, определяющий его положение и размеры. Если родительский объект нуждается в перерисовке, он отправляет сообщение всем своим дочерним окнам, запрашивая их растровое изображение. Затем дети ответят тем же сообщением, которое они обычно отправляют, когда решают, что им нужно перерисовать себя.

Эйрик

Чтобы процитировать MSDN страницу для WM_PAINT:

Сообщение WM_PAINT генерируется системой и не должно отправляться приложением. Чтобы заставить окно рисовать в определенном контексте устройства, используйте сообщение WM_PRINT или WM_PRINTCLIENT. Обратите внимание, что для этого требуется, чтобы целевое окно поддерживало сообщение WM_PRINTCLIENT. Наиболее распространенные элементы управления поддерживают сообщение WM_PRINTCLIENT.

Похоже, что вы можете перебирать все дочерние окна с помощью EnumChildWindows и отправлять им WM_PRINT с DC вашей памяти между шагами 5 и 6.

Это будет выглядеть примерно так:

static BOOL CALLBACK MyPaintCallback(HWND hChild,LPARAM lParam) {
    SendMessage(hChild,WM_PRINT,(WPARAM)lParam,(LPARAM)(PRF_CHECKVISIBLE|PRF_CHILDREN|PRF_CLIENT));
    return TRUE;
}

void MyPaintMethod(HWND hWnd) {
    //steps 1-5

    EnumChildWindows(hWnd,MyPaintCallback,MyMemoryDC);

    //step 6
}
Другие вопросы по тегам