「石鹸」はWPFのどこから来て、どのように対処するのか





このガイドは、アプリケーションで可能な限り鮮明な画像を取得しようとしているWPF開発者を対象としています。 骨の骨髄へのグラフィックシステムWPFはベクトルですが、その作業の最終結果は依然としてラスターです。 この事実に十分な注意を払わないと、さまざまな種類の「石鹸」、つまりラスター化の寄生アーティファクトが発生する可能性があります。 そのような状況では、心の存在を失わないことが重要であり、その発生の理由は非常に合理的であり、闘争の方法は非常にシンプルで効果的です。



目次



はじめに

1.ラスター画像のスケーリング

2.ピクセルサイズの倍数ではない座標

3.ラスター画像のネイティブ解像度

4.ベクター画像のラスタライズ

5.テキストを垂直に移動する

6. SnapsToDevicePixelsプロパティの使用

7.自己描画コントロール

おわりに

参照資料



はじめに



ラスタライゼーションアーティファクトの陰湿性は、それらが目立たないという事実にあります。 多くの開発者は、1ピクセルまたは2ピクセルのサイズの欠陥に気付かないだけです。 ただし、これらのささいなことがアプリケーションのユーザーエクスペリエンスに影響します。



注意力の小さなテスト:









さらに、上の図と下の図を区別する要因、およびそれらを除去する方法について説明します。 アプリケーションにこのような問題がない場合は、Windows設定でテキストとインターフェイス要素を増やすモードをオンにしてみてください。ほとんどの場合、問題が発生します。 この機能は、高解像度の小さな画面または弱視の人だけがよく使用します。









すべてをすべて読む必要はありません。イラストの表示に制限することもできます。 おそらくそれらを覚えており、アプリケーションのグラフィック出力を明確にするために本当に戦う必要がある場合は、このガイドに戻ります。



マニュアルの各セクションには、問題の問題とその解決方法を示すデモアプリケーションが備わっています。 コンパイルされたモジュールとそのソースコード(VS2010プロジェクト形式)を含む単一のアーカイブ(104 Kb)ですべてをダウンロードできます。



それでは、WPFはどこから来て、どのように対処するのでしょうか?



1.ラスター画像のスケーリング



ビットマップ画像で作業する場合、ぼかしの最も一般的な原因は出力中のスケーリングです。 画像の物理的なサイズと一致しない画像要素のサイズを指定すると、結果はソースに似なくなります。 イメージサイズをコンテナサイズに自動的に調整すると、多くの場合、同様の結果になります。 この場合、アーティファクトが発生する理由は、あるラスタグリッドから別のラスタグリッドに画像を転送する必要があるためです。











対抗


画像がスケーリングコンテナーに誤って表示された場合は、そこから取り出す必要があります。 画像の寸法が正しくない場合は、調整する必要があります。 すべてがシンプルです。 ただし、標準設定でアプリケーションをテストする場合に限ります。 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; } }
      
      





次に、出力の現在の解像度に応じて、標準画像の後継で画像の寸法を個別に設定できます。 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.ピクセルサイズの倍数ではない座標



計画外のぼかしを作成するかなり簡単な方法は、コンテナ内の画像を中央に配置することです( 四角形や他のShapeの子孫でも機能します)。 半分の場合、コンテナの幅は完全に半分に分割されていません。



ただし、センタリングせずに実行できます。 たとえば、 グリッドコンテナを使用すると、任意の割合で自分自身をパーツに分割できます。 左上のセルですべてが正常であり、右下のセルで霧があり、残りが他の何かである典型的なケースを次に示します。



 <!--      --> <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>
      
      











キャンバスのインデントまたはピクセルのサイズの倍数ではない位置にタスクの画像を塗り付けるのに効果的です。



これらのすべての場合、出力中にスケーリングが発生しなくても、「石鹸」が得られます。 画像のピクセルの境界は、画面のピクセルの境界内に収まらず、ラスタライズされると塗りつぶされます。









対抗


コンテナの場合、すべてがシンプルです。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にスケーリングなしの魔法の出力モードがある場合は、ほとんどの場合googleを試行します。 そのようなモードがあります。それはStretch = "None"です 。しかし、あなたは危険にさらされているので、それに頼って出力サイズの明示的なタスクを削除する必要があります。 ラスター画像には独自の解像度があり、メタデータで示され、WPFは画像の寸法を形成するときにそれを考慮します。 それが何であるかわからない場合、一連の成功した状況下で、あなたは黒魔術を信じることができます:あなたが持っている画像のいくつかは期待どおりに描かれ、それらと完全に類似したものは同じ条件下で膨張または収縮します。



実験用に次の3つの画像をダウンロードします(それぞれ解像度が異なります)。







画像の解像度が出力の解像度と一致しない場合、そのサイズを読み込むときに解像度の比率が乗算され、「元の」サイズでレンダリングするときに突然スケーリングが発生します。 ちなみに、最初のメソッドで示した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に設定する必要があります。 解像度を変更しても画像自体には何も起こりません。メタデータのみが変更されます。



標準ペイントはこれには適していません。もっと深刻なものが必要になります。 人気のある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.ベクター画像のラスタライズ



「石鹸」の出現の最初の3つの理由の説明から、WPFのcビットマップには1つの問題があるという十分に根拠のある意見があるかもしれません。 実際、ベクター環境で作業するには、ベクター画像を使用する方がはるかに自然です。 最初の実験では、SVGからXAMLへの変換は万能薬のようであり、サイズとピクセルについて考える必要がなくなりました。 悲しいかな、これはそうではありません。 真夜中になると、キャリッジはカボチャに変わり、ベクトル画像は表示用にラスタライズされます。



出力に含まれるピクセルが少ないほど、アーティファクトが多くなります。 48ピクセル以下のサイズの画像(これはデスクトップアプリケーションのすべてのグラフィックスのほぼ80%です)では、状況は次のように縮退します:ベクター画像は1つの解像度のみで正しくラスタライズされ、それ以外の場合に最適化されます。 準備された間違ったサイズでベクトルアイコンを表示すると、容赦ないアンチエイリアシングはあなたを待たせません。















対抗


場合によっては、画像サイズを単純に増やすだけで対応できます。 たとえば、ツールバーのボタンには32x32ピクセルのサイズの画像を使用し、コンテキストメニューには25x25のアイコンを使用します。 ただし、ベクトルアイコンのラスタライズ方法が本当に重要な場合は、特定の解像度に最適化する必要があります。画像の必要な詳細は、出力ラスタのピクセル境界と一致する必要があります。



5.テキストを垂直に移動する



テキストを表示するとき、WPFはいくつかのシャープ化手法を使用します。 テキストは静的ですが、その位置と選択されたラスター化モード(.NET Framework 4.0以降)は可能な限り明確に見えます。 垂直方向に移動すると、特定のせん断値で「sharpilka」が急激にオフになり、その後徐々にオンに戻ります。



次に、テキストアニメーションが押されたときにボタンに表示される偽のぼかし効果の例を示します。



 <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でのテキストのネスト(実際の状況ではボタンにも画像がありました)、およびテキストの1〜2ピクセル下への急激なシフトの組み合わせが機能しているようです。



対抗


.NET Framework 4.0以降では、 TextOptions属性を使用して、 IdealDisplayの 2つのテキストラスタライズモードから選択できます。 これにより、不快なぼかし効果がわずかに減少します。 フレームワークの以前のバージョンでは、ラスタライズモードは理想モードに対応しています。文字はピクセルグリッドに収まり、ラスタライズされます。 表示モードでは、中間処理が使用されます。水平テキストは常にピクセルに明確に添付され、同じ文字が同じようにラスタライズされます。 テキスト出力モードの詳細については、 こちらこちらをご覧ください



急なテキスト移動のぼかし効果は、実験室で簡単に再現できます。 ピクセルのサイズの倍数ではない位置に垂直に移動するだけで十分です。 以下は、3つのテキストブロックの平行移動の例です。 同じ出力モードの上の2つのブロックは、異なってぼやけています。 重要なのはシフト値ではなく、ラスタグリッドに対するテキストの位置です。









テキストを移動するという単なる事実は、必ずしも「動的なぼかし」につながりません。 この効果が発生すると、行全体に影響します。 残念ながら、開発者にはこの効果を制御する手段がありません。 場合によっては、ピクセルの境界に沿ってブロックの座標を揃えるか、経験的にシフトの量を選択することにより、その発生を回避できます。



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に設定することにより、ピクセルバインディングのコントロールを有効にできます (これを行うには、ルートグリッドでこの属性を設定するだけです)。 結果はより安定します。







ただし、画像は移動しても不変ではなく、理想的にはピクセルにスナップされません。 テレビ画面は両方の軸で1ピクセル以内で揺れ、ボタンは常にぼやけています。



SnapsToDevicePixelsプロパティをTrueに設定すると、レンダリング時に視覚要素が画面ピクセルの境界内に収まるようになります。 各コントロールは、異なる熱意と異なる方法でこれを行うよう努めます。 たとえば、 ImageLabel、およびTextBlockは、この属性とはまったく無関係です。 ソースジオメトリが成功した場合のみ、 ラインはピクセルにヒットします。 反対に、 Rectangleはズボンから飛び出し、常にピクセルにヒットします。



より安定したピクセルスナップを行うには、元の画像を調整する必要があります。

  1. ボタンを表す線のすべてのY座標に0.5を追加して、ボタンのエッジがコントロールスペースのピクセルグリッドと一致するようにします。
  2. TV画面の整数、たとえば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は、ピクセルにスナップするための特別なツールを提供します- ガイドライン 。 コントロールをレンダリングするための一連のアクションを形成する段階で(これが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の標準解像度でのみ正しく機能します。96dpiでは、ペンの幅がピクセルサイズと一致します。 他の解像度で明確な境界線を実現するには、各側面にガイドを割り当てる必要があります。







重要なニュアンスは、ガイドがスタックを介してDrawingContextと対話すると同時に、ガイドが含まれるシェイプだけでなく、現在の出力全体に影響を与えることです。 そのため、この例では、2本の平行線を揃えるために、各軸に1つのガイドのみが使用されています。 使用されているすべてのガイドを収集し、それらを一度にスタックにプッシュすると、結果は悲惨なものになります。 競合のため、それらの一部のみが機能し、残りは無視されます。



ガイドに沿ったピクセル境界への位置合わせは両方向で実行されるため、異なる状況では、画像の異なる部分を異なる方向に移動できます。 写真を動かすと、洗濯機の部品が互いに相対的に巻き取られ、状況によっては彼女の足が消えます。 前のセクションで行ったように、特定の解像度で安定した出力を最適化することで画像を変更できますが、どの解像度でも安定した出力を達成することはできません。 以下では、この欠点がない、ピクセルにスナップする別の方法を検討します。



代替の反作用


コントロールを自分で描画するときに、ガイドを使用してピクセルにスナップする必要はありません。 サイクリング愛好家は、レンダリングされたプリミティブを手動で調整することにより、ピクセルの境界内に入ることができます。 見た目ほど難しくありません。 次の条件が必要です。



  1. ソースデータの座標は、ピクセルの境界内に収まる必要があります。96 dpiの場合、Math.Roundを使用できます。一般的な場合、特定のピクセルサイズに丸める必要があります。
  2. 使用される羽の幅は、ピクセルサイズの倍数でなければなりません。
  3. ペンの幅に奇数のピクセルが含まれる場合、表示されるプリミティブの座標はピクセル幅の半分だけシフトする必要があります。
  4. コントロールを表示するときは、ラスタグリッドに対する座標のシフトを修正し、その動きのいずれかでOnRender再起動する必要があります。


最初の2つのポイントは、このような静的クラスを使用して実装できます(そのフラグメントは最初の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メソッドを使用する必要があります



独立して描画されたコントロールの基本クラスとして、3番目と4番目の条件(コントロールのサブピクセルシフトとフェザーの奇数サイズの補正)を実装すると便利です。



 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 Word 2010の統合ベクトルグラフィックエディターの要素は次のとおりです。







おなじみのアーティファクト?このような検出された問題の除去が技術的な問題である場合、このガイドの目的は達成されています。ご清聴ありがとうございました!



参照資料



MSDN-WPFアプリケーションでのピクセルスナップショット

MSDN-UIElement.UseLayoutRoundingプロパティ

Pete Brown- フォントとテキストレンダリングオプションの選択Wisely

MSDNブログ-WPF 4.0テキストスタックの改善

MSDN-方法:図面へのGuidelineSetの適用

MSDN-UIElement.SnapsToDevicePixelsプロパティ



Originalデモコード:ダウンロード(104 Kb)



イラストをありがとう:



romson



melkopuz



sevendot



All Articles