15000 FPS. Хардкорные трюки ч.2 — ну cовсем нестандартное окно

в 3:55, , рубрики: .net, windows, Дизайн в IT, ненормальное программирование, метки: , ,

Стремимся к «pixel perfect» + прячем окно от RDP!

imageЭто продолжение серии постов с префиксом «15000 FPS», начатло тут: часть 1 и часть 1.5. Можно и к этому окну добиться 15К FPS, но разумный подход тут не долбить FPS в цикле Render(), а перерисовывать лишь при необходимости, а большую часть остальной работы за нас винда сама сделает. Глядя на скриншот, первая мысль кодера — «ха, да мы все умеем делать нестандартные окна!».
Но дизайнер поопытнее заподозрит неладное: тень от окна какая-то не виндовая, и вообще тут градиенты и альфа-смешивание, не обошлось без честных 8 бит на альфа-канале. Как?
А нужен только Win32 API + System.Drawing.Bitmap, работать будет даже на Win2K с .Net 2.0 и это окно великолепно и быстро масштабируется и перемещается без глюков.
«Кастомное альфа-смешивание окна на десктоп и не тормозит? Вы шутите?».

Совсем не шучу. Итак, для начала подготовим нарезочку для будущего скина этого окна.
image Поскольку, статья публикуется также в блоге «Дизайн», то прошу дизайнеров меня не пинать за недостаточный «Pixel Perfect» — я сам делал эту нарезку… Старпёры-кодеры вроде меня успевают к своим годам и с фотошопом познакомиться, и музыку в Logic научиться писать, и при сильной необходимости, я сам себе мольберт и пианино, насколько умею.
Но можно сразу заметить, что закругления углов уже на порядок лучше родных виндовых — а все потому, что винда использует векторный REGION, который ни разу не антиалясится, а я читер.
Но конечено, можно добиться лучшего результата, если привлечь настоящего дискретного дизайнера. Кому из дизайнеров окажется дальнейший текст скучным, можно сразу идти за полным проектом с исходниками и семплом на CodePlex в конце статьи. И мучить своих кодеров, что у вас есть сотни идей и фишек для окон и контролов сразу из фотошопа в 32-bit PNG (только не говорите, от кого про это узнали, спасибо за такие приключения кодеры мне точно не скажут).

Как видите, у нас есть 4 угловых элемента, между которыми будем растягивать средние элементы, а основной фон окна заливать через:

GFX.Clear(WindowFillColor);

Да, всё наше окно это тупо System.Drawing.Bitmap, но кто читал прошлые части статьи знают, как я к этому неравнодушен. Из кусков скина легко собирается фон окна простыми операциями на Graphics.
(Для скриншота в заголовке я вместо этого использовал GradientBrush, но это просто спецэффект)

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

Автор: Lincoln6Echo

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js