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

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

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

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

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

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

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

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

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

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

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


person Eirik Vea    schedule 02.11.2011    source источник
comment
Самое простое, что можно сделать здесь, это использовать подход SetLayeredWindowAttributes вместо UpdateLayeredWindow; вы потеряете попиксельную альфа-канал, но в противном случае окно будет работать как обычное окно Win32 с дочерними окнами. (Используйте растровое изображение в качестве фона родительского окна и используйте цветовой ключ, чтобы получить неправильную форму). Уловка с самостоятельной компоновкой дочерних окон заключается в том, что вам придется делать это «постоянно», чтобы они оставались обновленными, даже когда пользователь взаимодействует с ними (например, вводит символы в редактирование с мигающей вставкой).   -  person BrendanMcK    schedule 03.11.2011
comment
Привет! Да, возможно, вариант SetLayeredWindowAttributes. Я использовал это раньше, чтобы сделать все окно прозрачным. Я, конечно, согласен с тем, что это немного упростит ситуацию, но при потере попиксельной альфы я также потеряю приятный эффект смешивания, который мне нужен, верно? Или я неправильно понимаю используемый вами термин цветового ключа? Я знаю, что для обновления элементов управления при взаимодействии с пользователем потребуется определенное количество рабочих мест. Мигающая вставка - хороший пример ...   -  person Eirik Vea    schedule 03.11.2011
comment
Да, вы теряете попиксельную альфу, поэтому плавного перехода не получается. Цветовой ключ - это то, что позволяет вам иметь неправильную форму - вы выбираете один цвет, который считается прозрачным, поэтому области в вашем окне, которые имеют этот цвет в качестве фона, будут прозрачными. Но это бинарно, включено или выключено, и не смешивается.   -  person BrendanMcK    schedule 03.11.2011


Ответы (3)


Думаю, я выберу такое решение:

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

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

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

Дочерние окна

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

Эйрик

person Eirik Vea    schedule 03.11.2011

Я пытался сделать то же самое. Из чтения этой и другой поисковой сети. Он объединяет рекомендуемый механизм для рисования 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 (), показанного выше. Вы можете увидеть полупрозрачный фон на правом краю представления и между кнопками.

введите описание изображения здесь

person KrisTC    schedule 04.09.2013

Чтобы процитировать страницу 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
}
person IronMensan    schedule 02.11.2011
comment
Привет и спасибо за ответ. Я попробовал, но в итоге элемент управления нарисован в верхнем левом углу окна как при использовании сообщений WM_PRINT, так и WM_PRINTCLIENT. И при изменении размера окна оно (все окно) постоянно прыгало вперед и назад в положение 0,0 на экране и туда, где оно было изначально до изменения размера. Не уверен, почему он это делает, нужно некоторое исследование, но, возможно, я смогу адаптировать этот метод в соответствии со своими потребностями. - person Eirik Vea; 03.11.2011
comment
Исправлена ​​ошибка с изменением положения окна на 0,0 во время изменения размера. Пришлось передать мне ТОЧКУ {0,0}, а не NULL в параметр функции UpdateLayeredWindow, определяющий размещение окна ... Типа идиотской ошибки. - person Eirik Vea; 03.11.2011