Стремимся к «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, но это просто спецэффект)
Я в статье коснусь ключевых моментов, и не буду описывать слишком много подробностей — на туториал не претендую (кому надо туториалы по .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, в одном из ВиО на хабре его рекламировали, надеюсь все будет ОК.
ссылка на оригинал статьи http://habrahabr.ru/post/165403/
Добавить комментарий