Стремимся к «pixel perfect» + прячем окно от RDP!
Это продолжение серии постов с префиксом «15000 FPS», начало тут: часть 1 и часть 1.5. Можно и к этому окну добиться 15К FPS, но разумный подход тут не долбить FPS в цикле Render(), а перерисовывать лишь при необходимости, а большую часть остальной работы за нас винда сама сделает. Глядя на скриншот, первая мысль кодера — «ха, да мы все умеем делать нестандартные окна!».
Но дизайнер поопытнее заподозрит неладное: тень от окна какая-то не виндовая, и вообще тут градиенты и альфа-смешивание, не обошлось без честных 8 бит на альфа-канале. Как?
А нужен только Win32 API + System.Drawing.Bitmap, работать будет даже на Win2K с .Net 2.0 и это окно великолепно и быстро масштабируется и перемещается без глюков.
«Кастомное альфа-смешивание окна на десктоп и не тормозит? Вы шутите?».
Совсем не шучу. Итак, для начала подготовим нарезочку для будущего скина этого окна.
Поскольку, статья публикуется также в блоге «Дизайн», то прошу дизайнеров меня не пинать за недостаточный «Pixel Perfect» — я сам делал эту нарезку… Старпёры-кодеры вроде меня успевают к своим годам и с фотошопом познакомиться, и музыку в Logic научиться писать, и при сильной необходимости, я сам себе мольберт и пианино, насколько умею.
Но можно сразу заметить, что закругления углов уже на порядок лучше родных виндовых — а все потому, что винда использует векторный REGION, который ни разу не антиалясится, а я читер.
Но конечно, можно добиться лучшего результата, если привлечь настоящего дискретного дизайнера. Кому из дизайнеров окажется дальнейший текст скучным, можно сразу идти за полным проектом с исходниками и семплом на CodePlex в конце статьи. И мучить своих кодеров, что у вас есть сотни идей и фишек для окон и контролов сразу из фотошопа в 32-bit PNG (только не говорите, от кого про это узнали, спасибо за такие приключения кодеры мне точно не скажут).
Как видите, у нас есть 4 угловых элемента, между которыми будем растягивать средние элементы, а основной фон окна заливать через:
GFX.Clear(WindowFillColor);
Да, всё наше окно это тупо System.Drawing.Bitmap, но кто читал прошлые части статьи знают, как я к этому неравнодушен. Из кусков скина легко собирается фон окна простыми операциями на Graphics.(Для скриншота в заголовке я вместо этого использовал заливку GradientBrush, но это только ради спецэффекта на скриншоте)
Я в статье коснусь ключевых моментов, и не буду описывать слишком много подробностей — на туториал не претендую, подробнее суть есть с сорцами на CodePlex, исходники там, как и в прошлых частях — просты как сапог, я старался.
Сразу маленький чит. В прошлых частях я был за полноценный alpha-канал, а тут, наоборот. Поскольку исходные битмапы скина не меняются при каждой перерисовке, можно смело использовать Premultiplied Alpha, и на старте приложения сделать так:
internal static Bitmap Shadow_L = Resources.Shadow_L.Clone(
new Rectangle(0, 0, Resources.Shadow_L.Width, Resources.Shadow_L.Height),
PixelFormat.Format32bppPArgb);
// и т.д. по каждому слайсу скина
т.е. берем из ресурсов приложения кусок скина, и прокачиваем его один раз, до [правильный термин «предварительно оптимизированного по альфе»?] и потом пользуемся им, из статик-класса, и этим сильно повышаем FPS. Профит.Как рисуем это странное окно?
Как и в прошлые разы, нам понадобится Win32 API.Нужно немного «магических констант»:
public const Int32 ULW_COLORKEY = 0x00000001;
public const Int32 ULW_ALPHA = 0x00000002;
public const Int32 ULW_OPAQUE = 0x00000004;
public const byte AC_SRC_OVER = 0x00;
public const byte AC_SRC_ALPHA = 0x01;
public const uint WM_SYSCOMMAND = 0x0112;
public const uint DOMOVE = 0xF012;
public const uint DOSIZE1 = 0xF001;
//...
public const uint DOSIZE8 = 0xF008;
public const uint SRCCOPY = 0x00CC0020;
Некоторые спросят: «Неужели Win32 держится на каких-то там цифрах?» Грубо говоря, да. Но в большинстве это решалось через неявные преобразования enum. Во времена 486-х процессоров концепция «Everything is an object» была бы непозволительной расточительностью!И немного импортов:
[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll", ExactSpelling = true)]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
public static extern TBool DeleteDC(IntPtr hdc);
// и т.д.
Ключевой метод #1 это при создании оконной формы:
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
if (!DesignMode)
cp.ExStyle |= 0x00080000;
return cp;
}
}
Суровые MFC-кодеры радостно узнают знакомые «CreateParams», но удивятся константе стиля.Но теперь наше окно стало чем-то вроде окна-импотента.
Ключевой метод #2. Представим себе, что мы до этого уже нарисовали вид нашего окна в Bitmap в памяти. Чтобы от него был толк:
private void AssignGFX()
{
IntPtr screenDc = Win32Helper.GetDC(IntPtr.Zero);
IntPtr memDc = Win32Helper.CreateCompatibleDC(screenDc);
IntPtr hBitmap = IntPtr.Zero;
IntPtr objBitmap = IntPtr.Zero;
try
{
hBitmap = BMP.GetHbitmap(Color.FromArgb(0));
objBitmap = Win32Helper.SelectObject(memDc, hBitmap);
TSize size = new TSize(Width, Height);
TPoint pointSource = new TPoint(0, 0);
TPoint topPos = new TPoint(Left, Top);
Win32Helper.UpdateLayeredWindow(Handle, screenDc, ref topPos, ref size, memDc, ref pointSource, 0, ref blend, Win32Helper.ULW_ALPHA);
}
finally
{
Win32Helper.ReleaseDC(IntPtr.Zero, screenDc);
if (hBitmap != IntPtr.Zero)
{
Win32Helper.DeleteObject(objBitmap);
Win32Helper.DeleteObject(hBitmap);
}
Win32Helper.DeleteDC(memDc);
}
}
«вынимаем» контент нашего System.Drawing.Bitmap BMP на окно.Чтобы мы могли передвигать и резайзить окно, уже не так страшно:
private void Form1_MouseDown(object sender, MouseEventArgs e)
{
if (.. /*правый нижний угол*/ ...)
{
Win32Helper.ReleaseCapture();
Win32Helper.PostMessage(Handle, Win32Helper.WM_SYSCOMMAND, Win32Helper.DOSIZE8, 0);
}
else if (e.Y < 28) /* верхушка окна */
{
Win32Helper.ReleaseCapture();
Win32Helper.PostMessage(Handle, Win32Helper.WM_SYSCOMMAND, Win32Helper.DOMOVE, 0);
}
}
Что насчет контролов? Забудьте про нормальные контролы, винда не станет их отображать на нашем недо-окне, вообще никак. Поэтому создаем свои собственные контролы, от интерфейса,
interface ISkinnableControl
{
void RedrawControl(Graphics GFX);
}
в котором каждый контрол сам себя «нарисует» как ему угодно на GFX от главного окна — я кнопки из GraphicsPath нарисовал, можно и из PNG взять какого-нибудь. А как же у меня на скриншоте нормальный Button нарисовался? Да как гвоздем прибил, так и нарисовался:
for (int cnt = Controls.Count - 1; cnt >= 0; cnt--)
if (Controls[cnt] is ISkinnableControl && Controls[cnt].Visible)
((ISkinnableControl)Controls[cnt]).RedrawControl(GFX);
else
Controls[cnt].DrawToBitmap(BMP, new Rectangle(/*W,H*/));
Конечно, к сожалению стандартный метод Control.DrawToBitmap() делает нам лишь «скриншот», контрола, зато теперь контрол видно, хоть и надо немного доп. бэкенд-кода для динамических обновлений его вида. Впрочем, наследников ISkinnableControl это тоже касается.Быстро ли это работает? Чертовски быстро. Сорцы и бинарник тестового приложения как всегда на CodePlex снова под MIT-лицензией, т.е. для как угодно, для чего угодно. Я не стал объединять с прошлым проектом, немного разные темы.
Почему это окно не видно по RDP? Потому-что при его рисовании винда использует какой-то хитрый оверлей экрана, поэтому это одновременно и быстро, и для RDP недоступно.
По RDP никаких глюков и черных прямоугольников, просто этого окна вообще нет, все нормальные окна под ним и над ним видны и работают отлично. Я это случайно нашел спустя уже несколько лет, после релиза готового приложения, я устал бегать с 3-го этажа на 1-й и обратно, думал юзеры издеваются, мне понадобилось кажется 3 пробежки, пока вспомнил про оверлей. Возможно сейчас, во времена Win7 что-то изменилось, и теперь это окно видно.
Если по поводу прошлой статьи евангелисты Microsoft могли лишь пригрозить пальцем:"Так делать не стоит", то теперь они скажут:
"Да ты вообще упоротый. Так использовать API, изначально созданный для рисования тени под курсором мышки?"
Да-да. Это API я нашел, когда в винде появились тени для мышки и меню. Хехе. Стал копать, как они рисуются, дальше было делом техники и размеров. Что-ж, теперь меня точно на работу в Microsoft никогда не возьмут.
https://alphawindow.codeplex.com/
Всем приятной пятницы!
P.S. Картинки на imageban.ru, в одном из ВиО на хабре его рекламировали, надеюсь все будет ОК.
UPD: проблема с памятью решена, мой косяк был. Исходники и бинарники обновил до 0.6.