WPF, WinForms: 15000 FPS. Хардкорные трюки ч.1.5

    Неожиданное продолжение этого поста (честно не ожидал резонанса), поэтому часть 2 хардкорных трюков, в которой речь пойдет немного о другом, пока подождет.
    Итак, в двух словах, что изменилось: добавлен контрол и тестовое приложение для WindowsForms, вариант WPF немного изменился, рефакторинг-причесалинг, добавился threadsafe и контрол теперь может нормально ресайзиться в рантайме (включено в сэмплы, но не советую разворачивать на полный экран — это реально пугает). Спасибо камрадам, указавшим на ошибки и недостатки и теперь теперь проект гордо 0.5 beta. Можно сразу отправиться за обновлением на razorgdipainter.codeplex.com/, кому интересны подробности прошу под кат.

    Речь пойдет о WinForms-варианте использования. Практически весь контрол:
    private readonly HandleRef hDCRef;
    private readonly Graphics hDCGraphics;
    private readonly RazorPainter RP;
    
    public Bitmap RazorBMP { get; private set; }
    public Graphics RazorGFX { get; private set; }
    
    public RazorPainterWFCtl()
    {
    	InitializeComponent();
    
    	this.MinimumSize = new Size(1, 1);
    
    	SetStyle(ControlStyles.DoubleBuffer, false);
    	SetStyle(ControlStyles.UserPaint, true);
    	SetStyle(ControlStyles.AllPaintingInWmPaint, true);
    	SetStyle(ControlStyles.Opaque, true);
    
    	hDCGraphics = CreateGraphics();
    	hDCRef = new HandleRef(hDCGraphics, hDCGraphics.GetHdc());
    
    	RP = new RazorPainter();
    	RazorBMP = new Bitmap(Width, Height, PixelFormat.Format32bppArgb);
    	RazorGFX = Graphics.FromImage(RazorBMP);
    
    	this.Resize += (sender, args) =>
    	{
    		lock (this)
    		{
    			if (RazorGFX != null) RazorGFX.Dispose();
    			if (RazorBMP != null) RazorBMP.Dispose();
    			RazorBMP = new Bitmap(Width, Height, PixelFormat.Format32bppArgb);
    			RazorGFX = Graphics.FromImage(RazorBMP);
    		}
    	};
    }
    
    public void RazorPaint()
    {
    	RP.Paint(hDCRef, RazorBMP);
    }
    

    Как и обещал, код прост, как сапог. Сразу обращает на себя внимание перенос GFX и BMP из кода тестового приложения в код контрола. Спасибо alexanderzaytsev, так и на душе спокойнее — фронтэнд-программеру будет сложнее сотворить что-то непотребное с ними, и с ресайзом контрола работать проще.

    Еще обращает на себя внимание lock() в коде ресайза. Как я уже упоминал, одно из основных преимуществ библиотеки — потоконезависимость. Мы можем не обращать внимания на UI Thread, и рисовать на контроле из любого потока, хоть из трех разных одновременно. Мы ведь не хотим, чтобы кто-то пытался рисовать на битмапе, который в данный момент пересоздается из-за ресайза?
    Соотвественно, фронтэнд-код оконного приложения тоже немного изменился:
    private void Render()
    {
    	lock(razorPainterWFCtl1)
    	{
    		razorPainterWFCtl1.RazorGFX.Clear((drawred = !drawred) ? Color.Red : Color.Blue);
    		razorPainterWFCtl1.RazorGFX.DrawString("habr.ru", this.Font, Brushes.Azure,10,10);
    		razorPainterWFCtl1.RazorPaint();
    	}
    }
    

    Еще один lock(), но мы можем быть спокойны, что никто не сможет одновременно и рисовать и ресайзить.
    В WPF-варианте практически те-же изменения. Данная реализация threadsafe конечно топорная, но для демо тестовых приложений годится.
    В WF при закрытии окна успевает проскочить Exception — просто измените цикл рендера, потому-что:
    renderthread = new Thread(() =>
    {
    	while (true)
    		Render();
    });
    
    это действительно дурацкая идея для продакшна.

    Можно было бы в качестве дочернего контрола в WPF-варианте использовать контрол из WF-варианта, но что-то мне подсказывает, что лучше разделить WPF и WF ветки, поскольку при дальнейшем развитии могут возникнуть особенности использования, уникальные для каждой из архитектур. Но, возможно, я просто сам себя пугаю, и в дальнейшем ветки объединятся.

    Пользуйтесь на здоровье под MIT-лицензией, т.е. вообще как угодно в любых целях. У кого есть желание — подключайтесь к развитию библиотеки на CodePlex.

    UPD: Хабра-камрады sintez и nile1 убедили сделать
    /// <summary>
    /// Lock it to avoid resize/repaint race
    /// </summary>
    readonly object RazorLock = new object();
    
    с оговоркой на тестовой форме // better practice is Monitor.TryEnter() pattern, but here we do it simpler
    Действительно, и не сильно усложнилось, и правильнее стало. Обновил сорцы на кодеплексе, зарелизил 0.6 бета, спасибы всем участвовавшим написал.
    Метки:
    • +24
    • 15,2k
    • 8
    Поделиться публикацией
    Комментарии 8
    • +5
      Вот это поток сознания :)
    • +2
      lock(this) — bad practiсe. Потенциально может привести к дедлоку, если какой-то внешний код (вдруг) будет лочить сам объект контрола. Лучше завести отдельную локальную переменную а-ля object lockObject и делать lock(lockObject).

      тут подробные комментарии есть.
      • 0
        Плюсую! Потому-что сам с такой фигней уже воткнулся. Но, я не знаю что стоило выбрать приоритетом для статьи — железную надежность, или простоту восприятия и короткий код. Выбрал — простой короткий код. Тем, кто поопытнее, у них вопросов не возникнет, сами переделают библиотеку под себя, у них нет вопросов, и тестовые семплы WPF и WF им даже не нужны.
        А тем, кто только начал кодить, им лучше попроще показать, и если что постепенно, до хардкора доберутся с потоками поиграются, с дебагером. Насколько смог, чтобы всем польза и позитив были.
        • 0
          Немного добавлю. У меня был этот вариант с lockobject и методами BeginPaint() и EndPaint() во фронтэнде, которые через Monitor лочили. Но, стало выглядеть сложнее, хотя и правильнее с точки зрения продакшн. Вот честно, я побоялся напугать заморочками в коде не очень опытных камрадов.
          • 0
            Блин, кажется я комментарием выше сделал рекурсивную ловушку, и теперь эти камрады знают, что они чего-то не знают, что их может напугать.
            • +1
              не думаю, что если вы создадите еще одну приватную переменную с говорящим именем, это сильно усложнит читаемость кода, например:
              private readonly object syncObj = new object(); // Переменная, используемая для синхронизации потоков

              Не следует перегружать код, но если уж вы взялись за синхронизацию потоков, делайте ее правильно, зачем учить плохому?
              Для тех, у кого возникнут вопросы, есть примитивный пример в MSDN:
              msdn.microsoft.com/ru-ru/library/c5kehkcz.aspx
              • +1
                Убедили, сделано, сорцы обновил

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.