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



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

Оглавление


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

Введение


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

Небольшой тест на внимательность:


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


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

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

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

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


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



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

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

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

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;
    }

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

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

Далее можно в наследнике стандартного Image самостоятельно выставлять габариты картинки в зависимости от текущего разрешения вывода. В XAML такая картинка должна размещаться без указания конкретных размеров.

public class StaticImage : Image
{
    static StaticImage()
    {
        //Отслеживание смены исходной картинки
        Image.SourceProperty.OverrideMetadata(
            typeof(StaticImage), 
            new FrameworkPropertyMetadata(SourceChanged));
    }

    private static void SourceChanged(  DependencyObject obj, 
                                        DependencyPropertyChangedEventArgs e)
    {
        var image = obj as StaticImage;
        if (image == null) return;

        //Поправка размера картинки под текущее разрешение
        image.Width = image.Source.Width * Render.PixelSize;
        image.Height = image.Source.Height * Render.PixelSize;
    }
}



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



2. Координаты, не кратные размеру пикселя


Достаточно простой способ получить незапланированное размытие — центрировать Image в контейнере (также работает для Rectangle и остальных наследников Shape). В половине случаев ширина контейнера не делится пополам нацело.

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

<!-- Габариты не делятся пополам нацело -->
<Grid Width="117" Height="117">        
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <!-- Четыре идентичных картинки -->
    <Grid.Resources>
        <Style TargetType="Image">
            <Setter Property="Width" Value="48"/>
            <Setter Property="Height" Value="48"/>
            <Setter Property="Margin" Value="5"/>
            <Setter Property="VerticalAlignment" Value="Top"/>
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="Source" Value="CookingPot.png"/>
        </Style>
    </Grid.Resources>

    <Image Grid.Column="0" Grid.Row="0"/>
    <Image Grid.Column="1" Grid.Row="0"/>
    <Image Grid.Column="0" Grid.Row="1"/>
    <Image Grid.Column="1" Grid.Row="1"/>
</Grid>


Не менее эффективно помогает размазать картинку задание в Canvas отступов или позиций, не кратных размеру пикселя.

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


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

С контейнерами всё просто, установка свойства UseLayoutRounding в True заставляет контейнер (Window в том числе) автоматически округлять положение дочерних элементов до ближайшего целого значения пикселей. В остальных случаях придётся, так или иначе, явно привязывать координаты к границам пикселей.

Заметьте, что выражение «целые координаты» означает «координаты, делящиеся на размер пикселя нацело» только в стандартном разрешении 96 dpi, во всех остальных Math.Round вам не поможет. В общем случае округлить координату до ближайшей границы пикселей можно так:

static public double SnapToPixels(double value)
{
    value += PixelSize / 2;

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

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

    return (int)div * PixelSize;
}


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


При первом столкновении с проблемой масштабирования растровых изображений вы, скорее всего, попробуете погуглить наличие у Image волшебного режима вывода без масштабирования. Такой режим есть, это Stretch=«None», но стоит на него понадеяться и убрать явное задание размеров вывода, как вы оказываетесь в группе риска. У растровых изображений есть своё собственное разрешение, оно указывается в метаданных, и WPF учитывает его при формировании габаритов картинки. Если вы не в курсе, что это такое, то, при удачном стечении обстоятельств, сможете поверить в черную магию: некоторые из имеющихся у вас изображений будут отрисовываться как положено, а полностью аналогичные им распухать или сжиматься при тех же условиях.

Скачайте эти три изображения для экспериментов (у каждого из них указано разное разрешение):

      

Если разрешение картинки не совпадает с разрешением вывода, то при загрузке его габариты умножаются на отношение разрешений, и при отрисовке в «оригинальном» размере вы внезапно получаете масштабирование. Кстати, приведённый в первом способе клаcc StaticImage также не защищён от этих искажений, так как опирается на свойства Source.Width и Source.Height.

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

<!-- Пиксельные размеры картинок одинаковы, разрешения разные -->
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>     
        
    <Image Grid.Column="0" Source="Man1.png" Stretch="None" Margin="5"/>
    <Image Grid.Column="1" Source="Man2.png" Stretch="None" Margin="5"/>
    <Image Grid.Column="2" Source="Man3.png" Stretch="None" Margin="5"/>
</Grid>




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

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

Стандартный Paint для этого не подойдёт, вам понадобится что-то посерьёзнее. В популярной бесплатной смотрелке 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 вы можете выбирать из двух режимов растеризации текста: Ideal и Display. Это позволит немного уменьшить неприятный эффект размытия. В предыдущих версиях фреймворка режим растеризации соответствует режиму Ideal — как буква на пиксельную сетку ложится, так и растеризуется. В режиме Display используется промежуточная обработка: текст по горизонтали всегда четко привязан к пикселям, а одинаковые буквы растеризуются одинаково. Более подробно про режимы вывода текста можно прочитать здесь и здесь.

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


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

6. Использование свойства SnapsToDevicePixels


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

<!-- Центрирующийся по обеим осям грид -->
<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 рекомендует визуальному элементу при отрисовке попадать своими границами в границы пикселей экрана. Каждый контрол будет стремиться это сделать с разным рвением и разными способами. Например, Image, Label и 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 направляющие взаимодействуют через стек и при этом влияют на весь текущий вывод, а не только на фигуры, в границы которых они попадают. Именно поэтому в примере для выравнивания двух параллельных линий используется только одна направляющая на каждую ось. Если собрать все использованные направляющие и разом затолкать в стек, то результат будет плачевным. Из-за возникшего конфликта сработают только некоторые из них, остальные будут проигнорированы.

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

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

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

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

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

//Информация о текущем разрешении и функции по попаданию в границы пикселей
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
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 62

    +7
    Колоссальный труд! Спасибо большое.

    ЗЫ: самовар это пасхалка на вариацию чайника Юта в рашн-эдишн?
      +4
      Как ни странно, самое времяёмким занятием оказалось сделать анимированные иллюстрации. Пришлось цепочку из четырёх разных программ задействовать, чтобы снять видео в GIF, кратно всё увеличить, обрезать и победить искажения палитры (и всё равно в разделе про размытие шрифтов пролезли проблемы с фоном анимашек). :)
        0
        ЗЫ: самовар это пасхалка на вариацию чайника Юта в рашн-эдишн?

        Да нет, просто мне нравится эта картинка и на ней видны проблемы с пикселями при их наличии. Это работа одного из участников конкурса трехцветных иконок, который я устраивал на gamin.ru: gamin.ru/blog/compo/7028
        +2
        Статья хороша! Жалко лишь, что не все актуально в современном WPF для WinRT и WP/Silverlight, где часть из описанного просто не работает (тот же BitmapScalingMode), а к основным источникам мыла — не целым координатам — добавляется еще и BitmapCache.
          0
          Дело в том, что руководство я написал исходя из собственной практики — с чем в работе не сталкивался тут не представлено. Буду благодарен возможности расширить свой «гербарий». Можете описать проблему с BitmapCache чуть подробнее?
            +3
            Сначала предыстория. На десктопе в WPF производительность довольно высока, но уже на Atom W8 планшетах или телефонах иногда нужны оптимизации. Свободы выбора в WPF на этих платформах нет никакой — нет шейдеров, управлением сглаживанием, собственных offscreen битмапов и пр. радости., но зато есть и часто используется FrameworkElement.CacheMode = new BitmapCache(); После включения такого режима результат вызова кешируется в текстуру на уровне GPU и текстура эта будет пересчитана при изменении параметров или содержания объекта.

            * BitmapCache is the only cache-mode that is supported.
            * The Caching is applied to the element and all of it’s child elements.
            * BitmapCaching should be used in scenarios where you are blending, transforming (translating, stretching, rotating).
            * Misuse of the CacheMode feature can hurt performance, so you need to really think through what you are doing. If your visual tree is interleaving cached and un-cached elements, you are effectively causing multiple rendering surfaces to get created behind the scenes. The un-cached surfaces are rendered in software and the cached surfaces are rendered in hardware. Your performance will be best if you can minimize the total number of rendering surfaces and get the hardware to do work where it can.


            Мыльность же возникает в основном при уменьшении картинки. Видимо, у закешированного варианта немного отличаются методы скейлинга (на GPU, а не на CPU сжимается?), или какая-то еще мелочь. Пороюсь в проектах, чтобы найти скриншоты, выложу сюда.
              0
              А бороться с этим размытием как? Обновлять кеш после изменения габаритов контрола?
                0
                Кеш обновится сам, это все работает прозрачно и без вмешательства. Бороться можно либо целиком отказываясь от кеша в критичном месте, либо следить, чтобы не было масштабирования картинок в принципе. Хорошо хоть на телефонах очень высокий PPI, это для бекграундов не заметно.

                P.S. В десктопной Windows 8.1 только что провел пару эксперментов в эмуляторе, мыла не было. Может, дело в отсутствии шейдеров у эмулятора, либо версии ОС. Полноценно потестирую на устройствах после работы.
                0
                >>Видимо, у закешированного варианта немного отличаются методы скейлинга
                Масштабируется только кэшированный bitmap. В WinRT по окончанию анимации происходит перерисовка bitmap'а.
              0
              WPF — это только и исключительно то, что на десктопе. Silverlight и WinRT — это XAML, да, но не WPF.
                0
                Мне с трудом представляется раскрытие всех прелестей платформы WPF без XAML. И вообще ваш комментарий какой-то сверх эмоциональный и до абсурда неинформативный.
                  +4
                  Никаких эмоций, просто корректировка терминологии. Судя по минусам, я несколько неудачно выразился. Попробую раскрыть смысл.

                  WPF — это тот фреймворк, который появился первым, и который работает только на десктопе (при этом XAML, несомненно, является его неотъемлимой составной частью). Silverlight — это не WPF. Это обособленный и самодостаточный фреймворк, который также использует XAML. Аналогично, UI-фреймворк в WinRT — это не WPF (официально он называется «WinRT XAML Framework», или просто «XAML Framework»).

                  Поэтому «современный WPF для WinRT» — это несколько бессмысленная фраза, которая может привести к путанице — особенно, если её читает новичок. Это примерно как сказать про JavaFX, что это «современный Swing для браузеров».
                    0
                    Название «XAML Framework» сложно назвать удачным, потому что XAML — это всего лишь язык разметки, и к гуям он гвоздями не прибит. После таких фраз новички ещё больше запутаются.

                    «WinRT XAML Framework» — это точно официальное название? А то результатов в Гугле по этому названию практически нет (30 можно считать статистической погрешностью).
                        +1
                        Если внимательно посмотреть на статьи на MSDN, то они старательно избегают дать какое-то определенное название всему этому делу. Пару раз мне встречались совсем уж неудобоваримые конструкции вроде «components in namespaces under Windows.UI». В 2011-м, когда все это дело только появилось, я спросил у разработчиков, как называть их детище при обсуждении на StackOverflow и тому подобных местах — и мне сказали, что это «Windows XAML [UI] Framework». На википедии соответствующая статья называется «Windows Runtime XAML Framework», правда, они не говорят, откуда взяли такое название.

                        Название, действительно, неудачное. Думаю, это прямая калька с пространства имен Windows.UI.Xaml. Но, увы, оно уже установилось — тот же MSDN постоянно говорит о «Store XAML apps» и тому подобных вещах, явно имея в виду далеко не только язык разметки.

                        Кстати, конкретно в WinRT, XAML к гую намертво еще не прибит, но уже приклеен — XamlReader теперь живет в Windows.UI.Xaml.Markup, и подружить его со своими негуевыми классами не так-то просто — т.е. это можно сделать, если руками реализовать IXamlMetadataProvider и т.д, но это все из разряда «если заведется, то можете ездить, но никаких обещаний». Кастомизация загрузки тоже практически нулевая.
                          +1
                          Сначала выкорчёвывали и отделяли, теперь назад пихают… Бардак. Интересно, как они следующий гуёвый фреймворк назовут и в какое пространство имён запихают.

                          System.Windows.Forms
                          System.Windows
                          Windows.UI

                          Будет просто Windows? %)
                            +3
                            Ну тут как раз пертурбации объяснимые :)

                            System.Windows.Forms — это когда дотнет еще не пытался быть всем для всех, а был просто высокоуровневой платформой для винды, а конкретно Windows Forms — это был по сути порт Windows Foundation Classes из J++. System — знак того, что он является частью стандартной библиотеки (это все еще до попыток стандартизации через ECMA/ISO).

                            Потом появляется Avalon, то бишь WPF, причем его официальная миссия — стать основным и главным UI-фреймворком под винду. Вплоть до того, что на нем должна быть написана сама винда (тогда еще Longhorn). С другой стороны, он избавился от завязок на Win32 API (всякие там хэндлы и window messages), и, теоретически, может быть кроссплатформенным. Соответственно, System.Windows резервируется под него как the GUI. В итоге авалон из винды выпилили, и лонгхорн стал вистой, но неймспейс остался.

                            Windows.UI — уже не System, т.е. не декларируется, как часть стандартной переносимой библиотеки .NET, а как чисто виндовый API.
                +1
                Спасибо за статью. Хороший анализ часто встречающейся проблемы и читается легко.
                  +1
                  Очень достойная статья.
                    +1
                    Бесценная статья, спасибо!
                      +7
                      Спасибо, статья отличная.
                      Сам давно занимаюсь интеграцией дизайна в XAML, все это уже пройденный этап.
                      От себя хочу добавить несколько моментов:
                      1) Использовать рендеринг текста Display — нужно очень аккуратно, тк в этом случае отключается рендер текста и контейнера, где он лежит, видеокартой, что приводит к глюкам и иногда еще большим артефактам чем размытие. Зачастую эффект плавающий.
                      2) Мы сейчас почти полностью отказались от растровой графики как таковой в XAML интерфейсах.
                      большинство иконок и графики рисуем в InkSkape и портируем графику и иконки прямо в XAML(объекты Path)
                      Почему InkSkape — потому что его родной формат SVG, он почти идентичен XAML и InkSkape позволяет рисовать вектор, привязываясь к пикселям сразу — для этого жмем Shift+«3» и видим пиксельную сетку, далее можно включить все привязки. тем самым никогда не попадаем на «полпикселя»
                      Ну и самый главный лайвхак — в InkSkape есть встроенный XML-Editor (Shift+Ctrl+X) — там просто копируем координаты объектов и вставляем в Data у Path в XAML.


                      Плоские иконки так делать пара пустяков и благодаря целочисленным координатам они всегда встают идеально в WPF, но можно делать очень сложную, и даже игровую графику, по сути полностью копируя стопку объектов в XAML, но там отдельной статьи материал о том как это делать эффективно.
                      Чтобы не быть голословным вот видео проекта что у меня сейчас в разработке, вся графика рисовалась в InkSkape и потом XML-Editor + прямыми руками вставлялась в проект и анимировалась. В проекте все векторное на 95%

                      http://youtu.be/gg8XmABwzkM

                      Если тема сообществу интересна, как завершу проект, могу написать статейку о процессе.
                        0
                        Хе… Аналогичный случай в нашем колхозе! 100% векторной графики в рабочих проектах. :]

                        Я пользуюсь open-source утилитой командной строки Svg2Xaml (перед этим приходится переводить всё в контуры, т.к. эта утилита ломает прямоугольники). Честно говоря не знал, что можно просто копированием переносить в XAML.
                          +1
                          Svg2Xaml штука все же глючная. В итоге я руками быстрее и качественнее все перевожу, кисти в любом случае в ресурсах, поэтому они мне как правило не нужны, и там проблема с группировкой объектов, из чего вытекает что SVG сначала нужно «подготовить» к экспорту, потом утилита, потом правки в XAML, я же работаю напрямую с исходником и кодом обновременно, поэтому лично для меня мой путь проще.
                          Совет
                          InkSkape может координаты представлять как в относительных, так и в «прямых» точках
                          первые начинаются с маленькой «m» вторые с большой «M», по умолчанию все в относительных, XAML с ними работает, но к сожалению иногда криво, чтобы перевести в «прямые» — выделяем объект и жмем в InkSkape CTRL + "+" — эта комбинация для объединения объектов, но так же она преобразовывает координаты если объект один.
                          А вообще в настройках выставляем не использовать относительные координаты и радуемся жизни.
                            0
                            Svg2Xaml штука все же глючная

                            Согласен. Но менее глючных аналогов я не наблюдаю, поэтому как-то притёрся. :) Спасибо за информацию, покопаю про относительные/абсолютные координаты подробнее.

                            Тема SVG > XAML интересная, явно просится на детальное рассмотрение. Казалось бы сущность WPF провоцирует разработчика перейти на векторные изображения, но при этом это никаких встроенных средств для этого нет. Только кустарные методы и собственные шишки. Было бы здорово, если бы вы поделились своим опытом.
                              +1
                              Это верно, когда первый раз видишь Path и примеры от майкрософт, первый вопрос — а как нарисовать что-то свое, на который нет ответа нигде, в примерах уже готовые объекты, Blend — не для рисования, Expression Disign — убог и поддерживает только Canvas и при экспорте — простите, говнокод, а графика как правило — адаптивная.
                              Мы даже думали над собственным редактором Path на панелях компоновки WPF, но сделать его — полбеды, как на него пересадить дизайнеров и научить работать хотя бы только с цветовыми ресурсами — вот где проблема, а иначе так же как и с Inkscape и Svg2Xaml половину кода руками переписывать.
                              Inkscape конечно решает большинство проблем, но не все, кое какие рутинные операции приходиться делать вручную. И все же он в разы облегчил работу над исходником дизайна, по крайней мере, там нельзя нарисовать того, чего нельзя «зазамлить» в WPF

                              У нас сейчас основная проблема, то что в XAML Windows 8 многого, что было раньше, просто нет. Нет триггеров, нет радиальных градиентов, опций рендара… Вот и приходиться иногда извращаться комбинируя растровые объекты с векторными. НО опять же Win8 в плане производительности на пару порядков выше и «мыла» в ней меньше чем в WPF.
                                0
                                Я рисую в Blend без проблем, для интерфейсов хватает на 99%. Делаю сразу не графику на Canvas, а полноценный лейаут.
                                Из FW или AI импортирую только звездочки-шестеренки.
                                  +3
                                  Вы меня неправильно поняли.
                                  Я имел ввиду создание дизайна в целом, а не только его реализацию в XAML.
                                  Да, все простые элементы с несложным дизайном (кнопки, текстовые блоки, списки и т.п). — бленд для них идеален и с ними все просто, И тут без разницы, где работал дизайнер, я как интегратор повторю все так же в бленде. Мы тут как раз о звездочках и прочих свистелках говорим и о быстрой сборке сложных дизайнов, и о взаимодействии дизайнера с интегратором.
                                  Если дизайнер рисует в AI например, то, чтобы выдрать от туда звездочку-шестеренку, нужно сначала подготовить исходник, а потом как-то экспортировать нужные элементы, потом вставить в бленд, потом причесать XAML(+ресурсы выставить, адаптивность и пр..)
                                  Inkscape позволяет просто открыть исходник как он есть — > Ctrl+ C — > Ctrl+V в бленд, где уже подготовлено все для вставки этой шестеренки., те я выкинул 2 промежуточных шага. При реализации больших проектов и сложных интерфейсов, это существенно
                                    0
                                    А ещё Inkscape бессовестно бесплатный, в отличии от AI и бленда.
                                      0
                                      Да, я потом посмотрел — вы рисуете игры. Конечно, для создания игровой графики инструменты Blend недостоточно удобны.

                                      Насчет «промежуточных шагов» не очень понятно. Сохраненый в иллюстраторе файл импортируется в Blend напрямую, без всяких промежуточных шагов. Да, он попадает не в выбранный контейнер, а в корневую директорию. Но если в Blend «всё подготовлено», то какая разница, куда? — Ctrl+ X — > Ctrl+V или взял да перетащил…
                                  0
                                  Я заранее прошу прощения если не прав. Но не так давно мне нужно было сделать векторную иконку в XAML и установленный InkScape прекрасно экспортировал нарисованное в нем непосредственно в XAML, без каких-либо промежуточных утилит.
                                    0
                                    К сожалению это происходит так гладко далеко не со всеми картинками.
                              0
                              Так и знал, что не удержишься от комментария =)
                              +1
                              Отличная статья!
                              Хотелось бы по подробнее узнать как в Inkspace рисовать привязанные к пикселям иконки, и экспортировать их в XAML.
                                +1
                                Спасибо! :)

                                Если парой слов: при редактировании делать в Inkskape сетку для привязок в полпикселя шириной. Конвертирую я утилитой командной строки svg2xaml (она больше не поддерживается, но того что есть хватает).
                                  +1
                                  Полпикселя нужны для привяки линий в один пиксель шириной. Перо отсчитывается в обе стороны от неё, поэтому для попадания края линии в границы пикселей она должна проходить точно по середине пикселя. Ну, конечно, привязка к сетке должна быть включена (по умолчанию настройки привязки в вертикальной панели справа). Сетка включается/отключается комбинацией Shift+3 (символ #).

                                  Иллюстрация с настройками сетки

                                  0
                                  Простите, чуть подправлю: редактор называется Inkscape. Это бесплатный open-source векторный редактор работающий с SVG-изображениями.
                                  +3
                                  Так много замыленных картинок в одной статье я давно не видел! После беглого просмотра, глаза заболели.
                                    +7
                                    Да уж, две трети иллюстраций с качеством ниже среднего. :D

                                    Занятно, что любой борец с чем-либо контактирует с этим гораздо больше остальных. Я в детстве мечтал стать ветеринаром, а потом передумал — поглядел как настоящий ветеринар вытаскивал овчарке клеща из уха. Ветеринар любит счастливых и здоровых животных, а работает преимущественно с больными и насчастными. Работа программиста, как оказалось, в этом плане ничем не отличается.
                                    +4
                                    Статья эпическая, я сам не верстальщик, но два года проработал с xaml верстальщиком плечом плечу и знаю сколько он добивался такого же результата путём проб, ошибок и экспериментов.
                                      +1
                                      Жизнь — боль.
                                        +1
                                        Самый фундаментальный труд из встречавших по теме, спасибо!

                                        Настоящая проблема с устранением мыла возникает при применении к элементам эффекта падающей тени. UseLayoutRounding работает очень избирательно, а когда работает, то поедает закругления мелких радиусов. SnapsToDevicePixels тоже не помогает.
                                          0
                                          А можно пример в XAML с поеданием углов? Очень любопытно.
                                            0
                                            Пожалуй, свои слова про поедание углов раундингом возьму назад.
                                            Только что сдублировал попап, чтоб показать разницу… Oбернул парочку WrapPanel, а резкость навелась сама собой. Похоже, что с углами просто не справляется видеокарта моей новой машины при DPI 100%.

                                            saveimg.ru/pictures/24-03-14/c62d79a4aad2051a25a42e54abde6016.png

                                            В-общем, если добавляешь падающую тень, поведение резкости непредсказуемо. Не могу поручиться за .NET 4.51, но в 4.5 я этого победить не мог.
                                              0
                                              Я могу ошибаться, но на приведённом скрине блоки с надписью «5W» идентичны. При этом они оба и горизонтальная линейка не попадают в границы пикселей по вертикали:



                                              Выше блока полоса в пиксель с размазанным красным, на самом блоке внизу полоса более тёмного красного. На линейке сверху видна полоса размытия (да и снизу тоже, забыл выделить). Явно есть маленький сдвиг вверх относительно точного попадания в пиксели. На горизонтальной линейке ведь нет эффекта тени? Да и, кстати, каким эффектом сделана эта тень?
                                                0
                                                Эффект — самый обычный DropShadowEffect. Пардон, пример был не очень удачный, вот более наглядный:

                                                saveimg.ru/pictures/25-03-14/2d9405e4b791e39cd23a9cc9eae1cbea.png

                                                Тень применена к попапу, к DataGrid и к маркерам (в данный момент они — красные). При этом кнопки внизу и вверху поплыли, а вот комбобоксы выглядят четко.

                                                Насколько я помню, при применении тени машина обсчитывает объект вместе с его тенью, и то, что размеры и обьекта, и тени задавались в целых пикселях, роли уже не играет — тень может быть обсчитана так, что получится не целое число, и появится мыло либо на всем объекте, либо на каких-то элементах.
                                                  0
                                                  Ага, думаю так и есть. Хорошо поплыло, и при этом опять вместе с линейкой. По-идее от этого вполне должны лечить UseLayoutRounding и SnapsToDevicePixels в нужных местах, если это конечно не самостоятельно отрисовывающиеся контролы.
                                          +1
                                          Вот спасибо! А то, честно говоря, это спонтанное замыливание уже задолбало.
                                            0
                                            Боюсь просто так замыливание не исчезает, придётся попотеть. :)
                                              +1
                                              Ну по крайней мере понятны методы борьбы. А то на одной машине показывает превосходно, а на другой — шлак. И кто виноват неясно. Короче говоря, спасибо за статью.
                                            0
                                            (ошибся веткой)
                                              +4
                                              Статья великолепная, но есть пара существенных замечаний:

                                              1) SnapsToDevicePixels не очень рекомендуется к использованию начиная с .NET 4. Вместо него следует использовать UseLayoutRounding (см тут msdn.microsoft.com/en-us/library/system.windows.frameworkelement.uselayoutrounding%28v=VS.100%29.aspx и тут stackoverflow.com/questions/2399731/when-should-i-use-snapstodevicepixels-in-wpf-4-0).

                                              Чтобы сделать Shape резким, следует использовать RenderOptions.EdgeMode=«Aliased» вместо SnapsToDevicePixels. Это решает проблему размытых векторных иконок в большинстве случаев.

                                              2) Использование Reflection для получения DPI — нехорошо. Правильный способ (CompositionTarget.TransformToDevice): stackoverflow.com/questions/1918877/how-can-i-get-the-dpi-in-wpf

                                                +1
                                                1) Спасибо за ссылки! По второй отличное разъяснение: SnapsToDevicePixels работает на этапе отрисовки, UseLayoutRounding работает на этапе формирования габаритов и разметки. Думаю этого знания достаточно, чтобы выбрать подходящий для ситуации инструмент.

                                                В любом случае, мне видится главным следующий факт — оба эти свойства рекомендательные, каждый контрол их имеет, но не каждый должным образом реализует. Мало того, у них мощные обобщающие названия, эти свойства создают видимость панацеи — выставил и порядок! А это совсем не так. Именно это я пытался донести в тексте руководства.

                                                2) Чем нехорошо использование Reflection для получения DPI? Приведённый «правильный способ» не универсален, т.к. требует для своего применения существующего и отрендеренного визуального контрола. Приходится лезть к родителю или к окну приложения, которое в данный момент может ещё даже не быть отрисованным.

                                                Если брать DIP непосредственно из того места, где он хранится, то данные о разрешении доступны из любого места, например уже в конструкторе вашего самостоятельно отрисовывающегося контрола. Собственно, по ссылке оба метода приведены.
                                                  0
                                                  Я имею ввиду что использование Reflection для доступа к приватным членам класса — это грязный хак и это последнее, что следует делать (надо объяснять, почему?). DPI можно получить другими способами, если требуется это сделать в отсутствие загруженного контрола (через WinApi).
                                                    0
                                                    Объяснять почему не нужно, я ниже параллельный коммент успел написать. :) Про легальный способ получения DPI не из родительского контрола очень интересно. Как?
                                                        0
                                                        Спасибо! Насколько я понял, самый корректный способ — запрашивать через GetDeviceCaps размер рабочего стола и экрана и из их отношения выводить DPI. В остальных случах есть какие-то проблемы на некоторых устройствах, если в манифесте приложения не указано, что оно «dpi aware».
                                                          0
                                                          А не указывать dpi awareness — это чем-то плохо?
                                                            0
                                                            Видимо тем плохо, что начинает сбоить выяснение реального DPI. Я с этим не сталкивался, но по ссылкам у ребят на планшете Surface Pro и каких-то других штуках проблемы вылезают — на 150% масштабирования шрифтов выдаётся 96dpi вместо ожидаемых 144dpi.
                                                              +1
                                                              Там, по тем же ссылкам, не исключают, что это некие локальные проблемы, и предлагают дёргать WMI. Мне-то кажется, что если необходимо работать с DPI, то правильно будет указывать dpi awareness.

                                                              Ещё вот сохранял себе когда-то ссылочку — High DPI Settings in Windows. Если честно, мне до сих пор так и не довелось столкнуться с dpi, отличным от 96, поэтому я здесь на правах диванного теоретика :)
                                                                0
                                                                Э… У нас даже среди коллектива разработчиков некоторые живут в мире увеличенных шрифтов и элементов интерфейса. А уж пользователи старше 50 почти поголовно на таких настройках.
                                                                  +1
                                                                  Ну вот недавно приобрёл планшет с Windows 8.1, и это первый мой девайс, на котором пришлось подкрутить DPI. Небольшая утилитка на Windows Forms ожидаемо поплыла :)
                                                    +1
                                                    Я слукавил, конечно, про Reflection, понятно, что выковыривание приватных членов класса снаружи чревато их исчезновением в какой-то неожиданный момент. Но в данном случае речь про настолько фундаментальный параметр, что встаёт несколько вопросов: почему DPI недоступен как публичный член класса? Почему без Reflection его нужно выковыривать из отрисованных контролов, если разрешение устройства вывода существует вне зависимости от отрисовки контролов?

                                                Only users with full accounts can post comments. Log in, please.