Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Откуда «мыло» в WPF и как с ним бороться

Anna | 17.06.2014 | нет комментариев
Это начальство для WPF-разработчиков, тяготящихся добиться максимально чёткой картинки в своих приложениях. Графическая система WPF до мозга костей векторная, но финальным итогом её работы по-бывшему является растр. Если не уделить этому факту надлежащего внимания, дозволено столкнуться с разными сортами «мыла» — паразитными артефактами растеризации. В такой обстановки значимо не терять присутствия духа, поводы их появления абсолютно разумны, а способы борьбы довольно примитивны и результативны.

Содержание

Вступление
1. Масштабирование растровых изображений
2. Координаты, не кратные размеру пикселя
3. Собственное разрешение растровых изображений
4. Растеризация векторных изображений
5. Перемещение текста по вертикали
6. Применение свойства SnapsToDevicePixels
7. Независимая отрисовка контролов
Завершение
Ссылки

Вступление


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

Маленький тест на наблюдательность:


Дальше речь пойдет о факторах, отличающих верхнюю картинку от нижней, и методах их устранения. Если в ваших приложениях таких задач нет, испробуйте включить в настройках Windows режим увеличения текста и элементов интерфейса — скорее каждого задачи появятся. Владельцы маленьких экранов с огромным разрешением либо легко люди со слабым зрением Зачастую пользуются этой функцией. 


Вовсе не непременно читать всё целиком и всецело, вы абсолютно можете ограничиться просмотром иллюстраций. Допустимо, они вам запомнятся, и вы вернётесь к этому начальству, когда вам подлинно потребуется бороться за чёткость графического итога вашего приложения.

Всякий раздел начальства снабжен демонстрационным приложением, иллюстрирующим рассматриваемую задачу и способы её решения. Вы можете скачать всё цельным архивом (104 Kb), содержащим скомилированные модули и их начальный код (формат планов VS2010).

Выходит, откуда же «мыло» WPF и как с ним бороться?

1. Масштабирование растровых изображений


При работе с растровыми изображениями самой частой поводом «замыливания» является масштабирование при итоге. Задайте элементу Image размеры, не совпадающие с физическим размером изображения, и итог теснее не будет схож на исходник. Механическая подгонка размеров картинки к размеру контейнера Зачастую приводит к аналогичному итогу. В данном случае повод возникновения артефактов, это надобность перевести изображение из одной растровой сетки в иную.

Противодействие


Если в масштабирующем контейнере изображение оказалось по ошибке, то его необходимо оттуда достать. Если размеры изображения неправильные, необходимо их откорректировать. Всё легко. Но только до тех пор, пока вы тестируете своё приложение со стандартными настройками. Если включить в Windows режим увеличенных шрифтов и элементов интерфейса, то разрешение итога WPF-приложения изменится, виртуальная единица womk!http://www.irfanview.com”>IrfanView вы можете задать разрешение в диалоге отображения свойств изображения (хоткей I):

В не менее бесплатном редакторе Paint.NET того же результата дозволено добиться зайдя в меню «Изображение», дальше «Размер полотна…» (хоткей Ctrl Shit R).

Если вы не хотите либо не можете трудиться с разрешением в графическом редакторе (скажем, ваше приложение работает с загружаемыми пользовательскими изображениями), вы можете поменять разрешение загружаемой картинки программно. Вот пример функции загрузки для 32-битных изображений:

//Загрузка Image.Source с принудительной установкой 96 dpi
BitmapSource ConvertBitmapTo96DPI(string path)
{
    var uri = new Uri(path);
    var bitmapImage = new BitmapImage(uri);

    int width = bitmapImage.PixelWidth;
    int height = bitmapImage.PixelHeight;

    int stride = width * 4; // 4 байта на пиксель
    var pixelData = new byte[stride * height];
    bitmapImage.CopyPixels(pixelData, stride, 0);

    return BitmapSource.Create( width, height, 96, 96, 
                                PixelFormats.Bgra32,
                                bitmapImage.Palette, 
                                pixelData, stride);
}

4. Растеризация векторных изображений


Из изложения первых трёх причин происхождения «мыла» у вас может сложиться абсолютно обоснованное суждение, что c растровыми изображения в WPF одни задачи. И подлинно, для работы в векторной среде значительно натуральней применять векторные изображения. При первых экспериментах реформирование SVG в XAML кажется панацеей, дозволяющей огромнее не задумываться о размерах и пикселях. Увы, это не так. Ровно в полночь карета превращается в тыкву, а векторное изображение растеризуется для итога на экран.

Чем поменьше пикселей на выходе, тем огромнее артефактов. На изображениях размером в 48 пикселей и поменьше (это чуть ли не 80% каждой графики в десктопных приложениях) обстановка вырождается в следующую: векторное изображение правильно растеризуется только в одном разрешении, под которое оптимизировано, в остальных теснее постольку от того что. Отобразите векторную иконку не в том размере, под тот, что её готовили, и неумолимый антиалиасинг не принудит себя ожидать.

Противодействие


В некоторых случаях дозволено обойтись простым увеличением размеров изображений. Скажем, для кнопок на панели инструментов применять картинки размером 32х32 пикселя, а для иконок контекстного меню 25х25. Однако, если вам подлинно значимо, как будет растеризоваться векторная иконка, то её необходимо оптимизировать под определенное разрешение — надобные детали картинки обязаны совпадать с границами пикселей выходного растра.

5. Перемещение текста по вертикали


При отображении текста WPF использует некую технику возрастания чёткости. Пока текст неподвижен, он выглядит максимально чётко для своего расположения и выбранного режима растеризации (.NET Framework 4.0 и выше). При перемещениях же по вертикали, при некоторых величинах сдвига «шарпилка» круто отключается, а после этого плавно включается обратно.

Вот пример паразитного результата размытия на кнопке с анимацией текста при нажатии:

<Button VerticalAlignment="Top">
    <Button.Template>
        <ControlTemplate TargetType="Button">
            <Border Width="255" Height="40"
                    BorderThickness="1 0 1 1" CornerRadius="0 0 10 10" 
                    BorderBrush="#FF202020" Background="#FFF7941D">
                <StackPanel Name="Panel" Orientation="Horizontal">
                    <Label    Content="Начните работу с нажатия этой кнопки" 
                            Foreground="#FF202020" VerticalAlignment="Center" 
                            Margin="20 0 0 0" Padding="0"/>
                </StackPanel>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Panel" Property="Margin" Value="3 1 -3 -1"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Button.Template>
</Button>


Данные появления размытия в этом примере достаточно таинственны. Судя по каждому, работает комбинация из отсутствия верхнего бордюра, вложенности текста в StackPanel (в реальной обстановки в кнопке была ещё картинка) и резкого сдвига текста вниз на один-два пикселя.

Противодействие


В .NET Framework 4.0 и выше с поддержкой признака TextOptions вы можете выбирать из 2-х режимов растеризации текста: Ideal и Display. Это дозволит немножко уменьшить малоприятный результат размытия. В предыдущих версиях фреймворка режим растеризации соответствует режиму Ideal — как буква на пиксельную сетку ложится, так и растеризуется. В режиме Display применяется промежуточная обработка: текст по горизонтали неизменно Отчетливо привязан к пикселям, а идентичные буквы растеризуются идентично. Больше детально про режимы итога текста дозволено прочитать тут и тут.

Результат размытия от резкого перемещения текста легко воспроизвести в лабораторных условиях. Довольно перемещать его по вертикали в расположения, не кратные размеру пикселя. Ниже показан пример параллельного перемещения трёх блоков текста. Два верхних блока с идентичным режимом итога размываются по-различному. Видимо, имеет значение не величина сдвига, а расположение текста касательно растровой сетки.

Сам факт перемещения текста не непременно приводит к «динамическому размытию». Если же данный результат появляется, то влияет на всю строку. Увы, средств для управления этим результатом разработчику не предоставляется. В некоторых случаях его появления дозволено избежать, если выравнивать координаты блока по границам пикселей либо подбирать величину сдвига опытным путём.

6. Применение свойства SnapsToDevicePixels


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

<!-- Центрирующийся по обеим осям грид -->
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
    <Grid.RowDefinitions>
        <RowDefinition Height="10"/>
        <RowDefinition Height="20"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="25"/>
        <ColumnDefinition Width="6"/>
    </Grid.ColumnDefinitions>

    <!-- Антенна (нет ни вертикалей, ни горизонталей) -->
    <Ellipse Grid.Column="0" Grid.Row="0" Grid.RowSpan="2"
             Fill="Black" Width="10" Height="10"
             VerticalAlignment="Top" Margin="15 5 0 0"/>
    <Line  Grid.RowSpan="2"
           X1="10" X2="20" Y1="1" Y2="11" Stroke="Black"/>
    <Line  Grid.ColumnSpan="2" Grid.RowSpan="2"
           X1="30" X2="20" Y1="1" Y2="11" Stroke="Black" />

    <!-- Ящик с экраном (у экрана дробный отступ) -->
    <Border Grid.ColumnSpan="2" Grid.Row="1" Background="#FFF7941D"/>
    <Rectangle Grid.Column="0" Grid.Row="1"
               Fill="White" RadiusX="3" RadiusY="3" 
               Margin="2.5"/>

    <!-- Кнопки (тощина линий 1, линии совпадают границами пикселей) -->
    <Line Grid.Column="1" Grid.Row="1" StrokeThickness="1"  
          Stroke="Black" X1="0" X2="4" Y1="3" Y2="3"/>
    <Line Grid.Column="1" Grid.Row="1" StrokeThickness="1"  
          Stroke="Black" X1="0" X2="4" Y1="5" Y2="5"/>
    <Line Grid.Column="1" Grid.Row="1" StrokeThickness="1"  
          Stroke="Black" X1="0" X2="4" Y1="7" Y2="7"/>
</Grid>


На иллюстрациях ниже контрол с демонстрационной картинкой перемещается шагами по 0,2 пикселя. Вначале он делает это раздельно по всякой из осей, потом по дуге окружности. В зависимости от фазы движения локальная координатная сетка контрола по-различному накладывается на физическую растровую сетку.

Изредка получаются Отчетливыми внешние границы телевизора, изредка границы его экрана, но это никогда не происходит единовременно. Кнопки Отчетливые либо по горизонтали, либо по вертикали, а Почаще размыты по обеим осям. Антенна в всякий комбинации немножко сглажена и по этому поводу не комплексует. 

Противодействие


Дозволено включить у контролов привязку к пикселям установкой свойства SnapsToDevicePixels в True (для этого довольно выставить данный признак у корневого грида). Итог будет стабильнее:

Тем не менее, картинка не является ни постоянной при перемещениях, ни безупречно привязанной к пикселям. Экран телевизора мотает в пределах одного пикселя по обеим осям, а его кнопки неизменно размыты.

Установка свойства SnapsToDevicePixels в True рекомендует визуальному элементу при отрисовке попадать своими границами в границы пикселей экрана. Всякий контрол будет тяготиться это сделать с различным рвением и различными методами. Скажем,ImageLabel и TextBlock относятся к этому признаку абсолютно индифферентно. Line будет попадать в пиксели только при успешной начальной геометрии. Rectangle, напротив, будет выпрыгивать из штанов и попадать в пиксели неизменно.

Для больше стабильной привязки к пикселям нужно откорректировать начальное изображение:

  1. ко каждому Y-координатам линий, изображающих кнопки, прибавить 0.5, Дабы их края совпали с пиксельной сеткой в пространстве контрола;
  2. сделать отступ у экрана телевизора целочисленным, скажем 2, Дабы его не мотало привязкой к ближайшим границам пикселей.


К слову, эти действия помогут получить Отчетливый и стабильный итог только в стандартном разрешении 96 dpi, в остальных же останется разброд и шатание. Для попадания в пиксели в любом разрешении следует обратиться к рекомендациям раздела «Независимая отрисовка контролов» (размеры контролов придётся корректировать на ходу, исходя из физического размера пикселя).

7. Независимая отрисовка контролов


Если вы перекрываете в своём визуальном элементе OnRender и отрисовываете его независимо с поддержкойDrawingContext, то задач растеризации у вас ровно столько же, сколько у преемников Shape из предыдущего метода, но при этом функциональность SnapsToDevicePixels вы обязаны реализовывать независимо. Если, безусловно, хотите. Дозволено не заморачиваться и делать приблизительно так:

public class Washer : FrameworkElement
{
    public Washer()
    {
        _brush = new SolidColorBrush(Color.FromRgb(247, 148, 29));
        _brush.Freeze();

        _pen = new Pen(Brushes.Black, 1);
        _pen.Freeze();
    }

    protected override void OnRender(DrawingContext dc)
    {
        //Ножки
        dc.DrawLine(_pen, new Point(1, 21), new Point(4, 21));
        dc.DrawLine(_pen, new Point(12, 21), new Point(15, 21));

        //Корпус
        var rect = new Rect(0, 0, 16, 21);
        dc.DrawRectangle(_brush, null, rect);

        //Кнопки
        dc.DrawLine(_pen, new Point(12, 1), new Point(12, 4));
        dc.DrawLine(_pen, new Point(14, 1), new Point(14, 4));

        //Окошко
        dc.DrawEllipse(Brushes.White, _pen, new Point(8, 11), 5, 5);

        //Защелка на окошке
        rect = new Rect(10, 10, 4, 2);
        dc.DrawRectangle(Brushes.White, _pen, rect);
    }

    private Pen _pen;
    private Brush _brush;
}


Невзирая на то, что все заданные координаты целочисленные, наложение на пиксельную сетку в пространстве контрола будет протекать так:


Ширина пера отсчитывается в обе стороны от заданной координатами линии. Если половина пера не делится на ширину пикселя нацело, то края линии не попадают в границы пикселей, даже если заданные координаты в них верно укладываются. Если изображение трудное, то может оказаться, что как изображение не сдвинь, а добиться, Дабы все его составляющие оказались чёткими не получается. На иллюстрации показано перемещение изображения с шагом в 0,2 пикселя.

Противодействие


WPF предоставляет особые средства для привязки к пикселям — направляющие (guidelines). На этапе образования цепочки действий по отрисовке контрола (именно этим занимается способ OnRender) вы можете указать вертикальные и горизонтальные координаты в пространстве контрола, которые при итоге обязаны попасть верно в границы пикселей.

В коде это выглядит так (только способ OnRender):

protected override void OnRender(DrawingContext dc)
{
    double halfPen = _pen.Thickness / 2;

    //Ножки
    var snapX = new double[] { 1, 12 };
    var snapY = new double[] { 21   halfPen };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));        
    dc.DrawLine(_pen, new Point(1, 21), new Point(4, 21));
    dc.DrawLine(_pen, new Point(12, 21), new Point(15, 21));        
    dc.Pop();

    //Корпус
    snapX = new double[] { 0, };
    snapY = new double[] { 21 };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));
    var rect = new Rect(0, 0, 16, 21);
    dc.DrawRectangle(_brush, null, rect);
    dc.Pop();

    //Кнопки
    snapX = new double[] { 12 - halfPen };
    snapY = new double[] { 1 };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));
    dc.DrawLine(_pen, new Point(12, 1), new Point(12, 4));
    dc.DrawLine(_pen, new Point(14, 1), new Point(14, 4));
    dc.Pop();

    //Окошко
    snapX = new double[] { 3 - halfPen };
    snapY = new double[] { 6 - halfPen };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));
    dc.DrawEllipse(Brushes.White, _pen, new Point(8, 11), 5, 5);
    dc.Pop();

    //Защелка на окошке
    snapX = new double[] { 10 - halfPen };
    snapY = new double[] { 10 - halfPen };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));
    rect = new Rect(10, 10, 4, 2);
    dc.DrawRectangle(Brushes.White, _pen, rect);
    dc.Pop();
}

Для работы с направляющими вам не необходимо знать нынешнее разрешение итога. Довольно расставить их надлежащим образом в локальных координатах контрола. Приведённый пример правильно работает только в стандартном разрешении 96 dpi, в котором ширина пера совпадает с размером пикселя. Дабы добиться Отчетливых границ линий в других разрешениях, придётся назначать по направляющей на всякую из её сторон.

Значимым нюансом является то, что с DrawingContext направляющие взаимодействуют через стек и при этом влияют на каждый нынешний итог, а не только на фигуры, в границы которых они попадают. Именно следственно в примере для выравнивания 2-х параллельных линий применяется только одна направляющая на всякую ось. Если собрать все использованные направляющие и разом затолкать в стек, то итог будет печальным. Из-за возникшего раздора сработают только некоторые из них, остальные будут проигнорированы.

Выравнивание к пиксельным границам по направляющим осуществляется в обе стороны, следственно в различных обстановках различные части изображения могут быть передвинуты в различные стороны. При перемещениях картинки части стиральной машинки мотаются друг касательно друга, а в некоторых обстановках у неё пропадают ножки. Изображение дозволено изменить, оптимизируя под стабильный итог в определенном разрешении, как было сделано в предыдущем разделе, но добиться стабильного итога в любом разрешении не получится. Чуть ниже рассмотрен альтернативный метод привязки к пикселям, лишенный этого недостатка.

Альтернативное противодействие

Для привязки к пикселям при независимой отрисовке контролов не непременно применять направляющие. Любители велосипедного спорта могут попадать в границы пикселей путём ручной коррекции отрисовываемых примитивов. Это не так трудно, как может показаться. Понадобится исполнять следующие данные:

  1. координаты начальных данных обязаны попадать в границы пикселей. Для 96 dpi дозволено воспользоваться Math.Round, для всеобщего случая придётся округлять по определенному размеру пикселя;
  2. ширина используемых перьев должна быть кратной размеру пикселя;
  3. в случаях когда ширина пера содержит нечётное число пикселей, координаты отображаемого примитива обязаны быть сдвинуты на половину ширины пикселя;
  4. при итоге контрола нужно делать поправку на сдвиг его координат касательно растровой сетки и перезапускать OnRender при всяких его перемещениях.

Первые два пункта дозволено реализовать с поддержкой такого статического класса (его фрагменты приводились в первых 2-х разделах):

//Информация о нынешнем разрешении и функции по попаданию в границы пикселей
public static class Render
{
    static Render()
    {
        var flags = BindingFlags.NonPublic | BindingFlags.Static;
       var dpiProperty = typeof(SystemParameters).GetProperty("Dpi", flags);

       Dpi = (int)dpiProperty.GetValue(null, null);
        PixelSize = 96.0 / Dpi;
        HalfPixelSize = PixelSize / 2;
    }

    //Размер физического пикселя в виртуальных единицах
    public static double PixelSize { get; private set; }

    //Текущее разрешение
    public static int Dpi { get; private set; }

    //Округление до границ пикселей
    static public double SnapToPixels(double value)
    {
        value  = HalfPixelSize;

        //На нестандартных DPI размер пикселя в WPF-единицах дробный.
        //Перемножение на 1000 необходимо из-за потерь точности
        //при представлении дробных чисел в double
        //2.4 / 0.4 = 5.9999999999999991
        //2400.0 / 400.0 = 6.0

        var div = (value * 1000) / (PixelSize * 1000);

        return (int)div * PixelSize;
    }

    private static readonly double HalfPixelSize;
}

Если какую-либо величину (скажем, ширину пера либо экранную координату) необходимо жёстко привязать к размеру пикселя, то довольно задать её как Render.PixelSize * n. Если же необходимо округлить её до значения, кратного размеру пикселя, то нужно воспользоваться способом Render.SnapToPixels.

Выполнение третьего и четвёртого условий (коррекции при сабпиксельных сдвигах контрола и нечётных размерах перьев) комфортно реализовать в виде базового класса для независимо отрисовываемых контролов:

public class SelfDrawingControlBase : FrameworkElement
{
    public SelfDrawingControlBase()
    {
        Snap = 0.5 * Render.PixelSize;
        SubpixelOffset = new Point(0, 0);
        LayoutUpdated  = OnLayoutUpdated;
    }

    protected void OnLayoutUpdated(object sender, EventArgs e)
    {
        FixSubpixelOffset();
        InvalidateVisual();
    }

    //Подгонка координат линии для точного попадания в границы пикселей
    protected void SnapLine(Pen pen, ref Point begin, ref Point end)
    {
        var snapX = -SubpixelOffset.X;
        var snapY = -SubpixelOffset.Y;

        if (IsOdd(pen.Thickness))
        {
            if (begin.X == end.X)
                snapX  = Snap;

            if (begin.Y == end.Y)
                snapY  = Snap;
        }

        begin.X  = snapX;
        begin.Y  = snapY;

        end.X  = snapX;
        end.Y  = snapY;
    }

    //Подгонка координат прямоугольника для точного попадания в границы пикселей
    protected void SnapRectangle(Pen pen, ref Rect rect)
    {
        var snapX = -SubpixelOffset.X;
        var snapY = -SubpixelOffset.Y;

        if (pen != null && IsOdd(pen.Thickness))
        {
            snapX  = Snap;
            snapY  = Snap;
        }

        rect.Location = new Point(rect.Left   snapX, rect.Top   snapY);
    }

    //Подгонка координат эллипса для точного попадания в границы пикселей
    protected void SnapEllipse(Pen pen, ref Point center)
    {
        var snapX = -SubpixelOffset.X;
        var snapY = -SubpixelOffset.Y;

        if (pen != null && IsOdd(pen.Thickness))
        {
            snapX  = Snap;
            snapY  = Snap;
        }

        center.X  = snapX;
        center.Y  = snapY;
    }

    //Половинка пикселя для привязки к пиксельной сетке
    protected double Snap { get; private set; }

    //Общий сдвиг контрола касательно границ пикселей
    protected Point SubpixelOffset { get; private set; }

    //Выяснение сдвига контрола касательно границ пикселей
    //для учёта его в последующей привязки к пикселям
    private void FixSubpixelOffset()
    {
        var offset = TranslatePoint(new Point(0, 0),
                                    Application.Current.MainWindow);

        SubpixelOffset = new Point( ModByPixel(offset.X),
                                    ModByPixel(offset.Y));
    }

    //Проверка на нечётное число пикселей
    private static bool IsOdd(double value)
    {
        //На нестандартных DPI размер пикселя в WPF-единицах дробный.
        //Перемножение на 1000 необходимо из-за потерь точности
        //при представлении дробных чисел в double
        //1.0 % 0.1 = 0.09999999999999995
        //1000.0 % 100.0 = 0.0
        return (value * 1000) % (Render.PixelSize * 2 * 1000) != 0;
    }

    //Остаток от деления на ширину пиксела
    private static double ModByPixel(double value)
    {
        return ((value * 1000) % (Render.PixelSize * 1000)) / 1000;
    }
}

Основная функциональность этого класса заключается в коррекции координат графических примитивов перед их итогом. Способы SnapXXX изменяют начальные данные таким образом, Дабы итог отрисовки попадал верно в границы пикселей.

Прямоугольники и эллипсы довольно сдвинуть целиком на половину пикселя при перьях нечётной ширины. У горизонтальных линий необходимо корректировать координату Y и не трогать координату X, у вертикальных — напротив. При коррекциях координат также учитывается сдвиг контрола касательно пиксельной сетки.

Привязка к пикселям в примере со стиральной машинкой:

public class Washer : SelfDrawingControlBase
{
    public Washer()
    {
        _brush = new SolidColorBrush(Color.FromRgb(247, 148, 29));
        _brush.Freeze();

        _pen = new Pen(Brushes.Black, 1);
        _pen.Freeze();
    }

    protected override void OnRender(DrawingContext dc)
    {
        //Ножки
        Point start = new Point(1, 21);
        Point end = new Point(4, 21);
        SnapLine(_pen, ref start, ref end);
        dc.DrawLine(_pen, start, end);

        start = new Point(12, 21);
        end = new Point(15, 21);
        SnapLine(_pen, ref start, ref end);
        dc.DrawLine(_pen, start, end);

        //Корпус
        var rect = new Rect(0, 0,16, 21);
        SnapRectangle(null, ref rect);
        dc.DrawRectangle(_brush, null, rect);

        //Кнопки
        start = new Point(12, 1);
        end = new Point(12, 4);
        SnapLine(_pen, ref start, ref end);
        dc.DrawLine(_pen, start, end);

        start = new Point(14, 1);
        end = new Point(14, 4);
        SnapLine(_pen, ref start, ref end);
        dc.DrawLine(_pen, start, end);

        //Окошко
        var center = new Point(8, 11);
        SnapEllipse(_pen, ref center);
        dc.DrawEllipse(    Brushes.White, _pen,
                        center, 5, 5);

        //Защелка на окошке
        rect = new Rect(10, 10, 4, 2);
        SnapRectangle(_pen, ref rect);
        dc.DrawRectangle(Brushes.White, _pen, rect);
    }

    private Pen _pen;
    private Brush _brush;
}

Итог получается аналогичным методу с направляющими, но при этом стабильным касательно перемещений контрола. Это происходит вследствие тому, что коррекция координат осуществляется только в одну сторону.

Приведённое решение примера работает только в стандартном разрешении 96 dpi — размеры пера и координаты примитивов для наглядности вставлены в коде определенными числами. Если вам нужно, Дабы изображение правильно привязывалось к пикселям в всяких разрешениях, то раньше чем передавать данные в способы SnapXXX, их необходимо округлять до границ пикселей способом Render.SnapToPixels.
Вот, скажем, контрол, рисующий прямоугольник, тот, что адекватно масштабируется при смене разрешения и попададает при этом в границы пикселей:

public class CrossDpiBrick : SelfDrawingControlBase
{
    public CrossDpiBrick()
    {
        _brush = new SolidColorBrush(Color.FromRgb(247, 148, 29));
        _brush.Freeze();

        _pen = new Pen(Brushes.Black, Render.SnapToPixels(7));
        _pen.Freeze();
    }

    protected override void OnRender(DrawingContext dc)
    {
        var rect = new Rect(Render.SnapToPixels(10),
                            Render.SnapToPixels(10),
                            Render.SnapToPixels(120),
                            Render.SnapToPixels(40));

        SnapRectangle(_pen, ref rect);

        dc.DrawRoundedRectangle(_brush, _pen, rect,
                                Render.SnapToPixels(10),
                                Render.SnapToPixels(10));
    }

    private Pen _pen;
    private Brush _brush;
}

Ручная привязка к пикселям требует чуть большего вмешательства в процесс отрисовки, чем при работе со встроенными средствами WPF, но разрешает больше эластично руководить этим процессом и достигать стабильного итога при любом разрешении итога.

Завершение


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

Для этого, в первую очередь, нужно примечать возникающие задачи растеризации. Это не так легко. Даже продукты от Microsoft не неизменно могут похвастаться безукоризненной картинкой. Вот, скажем, элементы встроенного редактора векторной графики в Microsoft Word 2010:


Знакомые артефакты? Если сейчас для вас истребление сходственных найденных задач — дело техники, цель данного начальства достигнута. Спасибо за внимание!

Ссылки

MSDN — Pixel Snapping in WPF Applications
MSDN — UIElement.UseLayoutRounding Property 
Pete Brown — Choose your Fonts and Text Rendering Options Wisely
MSDN Blogs — WPF 4.0 Text Stack Improvements
MSDN — How to: Apply a GuidelineSet to a Drawing
MSDN — UIElement.SnapsToDevicePixels Property

Начальный код демонстрационных примеров: скачать (104 Kb)

Спасибо за предоставленные иллюстрации:

romson

melkopuz

sevendot

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB