Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

WPF/Silverlight 의 그래픽 단위와 Anti-aliasing 처리를 이해하자


얼마전부터, 글을 쓰다 보니 왠지 이 정도의 내용은 마이크로소프트웨어 잡지에 올려도 괜찮지 않을까 싶은 경우가 생기더군요. 아래의 글이 그랬고,

SQL Server 2012 에 포함된 LocalDB 기능 소개
; https://www.sysnet.pe.kr/2/0/1258

이번 글이 또한 그랬습니다. 아래에 실린 내용이 원래 이 글을 썼던 '원본'입니다. 하지만 잡지사에 내려면 문체가 바뀌어야 하고, '지면의 압박'이 있어서 글의 내용이 다소 압축된 형태로 나가게 됩니다. 결국, 마이크로소프트웨어 잡지사에는 다음과 같은 내용으로 실렸습니다. (심지어 제목까지 바뀌었죠. ^^)

마이크로소프트의 새로운 그래픽 좌표 단위: DIU
; http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=39627

위의 글에 대한 오피스 워드 파일 원본은 이글에 실었으므로 참고하시고... 어떤 버전의 내용으로 글을 읽어도 전체적인 맥락은 같으므로 상관없으니... 각자 취향에 따라 읽어주시면 되겠습니다. ^^




지난 번 경험을 통해서,

WPF - 그리기 성능 및 Blurring 문제
; https://www.sysnet.pe.kr/2/0/1186

이번에는 DPI 와 WPF 그래픽 관계를 확실히 이해하고 넘어가야 할 때가 된 것 같아서 이렇게 정리해 봅니다. (사실, 제가 그래픽에는 약해서. ^^;)

참고로, 이에 대한 것은 역시 Petzold 의 글이 최고인 듯 싶습니다. ^^

The 96 DPI Solution
; http://www.charlespetzold.com/blog/2005/11/250723.html

위의 글을 한번 정리해 볼까요?

WPF 는 무조건 1/96 inch 에 해당하는 값을 좌표 단위로 사용합니다.
즉, WPF 에서 Rectanlge.Width="96" 인 사각형은 프린터로 출력 시 1 inch (2.54cm) 인 사각형이 출력되는 것입니다.

실제로 그런가 테스트를 해볼까요? 다음과 같이 길이가 (394 - 10) == 384 인 선을 긋고,

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">

    <Grid Name="gridView">
        <Path Stroke="Black" StrokeThickness="1" >
            <Path.Data>
                <LineGeometry StartPoint="10,20" EndPoint="394,20" />
            </Path.Data>
        </Path>
    </Grid>

</Window>

wpf_dpi_1.png

코드로 프린팅을 해봅니다.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        PrintDialog dialog = new PrintDialog();
        if (dialog.ShowDialog() == true)
        { 
            dialog.PrintVisual(gridView, "My"); 
        } 
    }
}

종이로 출력되었으면, 이제 직접 자를 대고 몇 cm 인지 재봅니다. 어떠세요? 제 경우에는 거의 101mm 정도가 나왔습니다.

WPF 384 == 384 / 96 == 4 inch
4 inch * 2.54 cm == 10.16 cm == 101.6 mm (1inch == 2.54cm)

계산값으로도 101.6mm 가 나오니 육안으로 확인한 101mm 라면 정확하다고 해야 겠지요. ^^

자... 이렇게 WPF 에서 사용되는 수치 단위를 "DIU(device-independent units)" 라고 부르고 있습니다. 1 DIU == 1 / 96 inch == 0.010416... inch == 0.0264583... cm 값입니다.

다음 단계로, 이제 윈도우 시스템에 설정된 DPI 값에 대해서 알아볼까요? 일반적으로 특별한 설정을 하지 않는 한 윈도우는 기본값으로 96 DPI 로 설정합니다. 따라서, 윈도우에서는 모니터에 96 pixel 이 연속으로 찍힌 경우 프린터로 찍어 보면 1 inch 의 선이 그어지는 것입니다. 기본값으로 본다면, WPF 의 DIU 단위와 윈도우의 DPI 단위가 같아서 1 DIU == 1 DPI == 1 pixel 관계가 됩니다.

그럼, 모니터에서 실제로 보여지는 크기를 자로 재보면 어떻게 될까요? 가령, 96 pixel 이 찍혔다고 해서 직접 모니터에 inch 잣대를 대고 계산하면 1inch 가 나올까요?

여기서 PPI(Pixel per Inch) != DPI(Dots per Inch) 임을 알아야 합니다. PPI 는 실제 모니터의 크기 안에 들어가는 pixel 수를 나타냅니다. (웹에서 검색된 자료들 중에는, 화면에서도 동일하게 1inch 로 나온다는 식의 설명을 볼 수 있는데 이는 몇몇 경우에만 사실이고, 일반적인 대다수의 사용자들에게는 사실이 아닙니다.)

이에 대해서는 아래의 글에서 잘 설명이 되어 있습니다.

Is WPF Really Resolution Independent?
; http://www.wpflearningexperience.com/?p=41

(참고로, 위의 설명에서는 PPI 를 Physical DPI 로 표현하고 있습니다.)

이에 따라 제 노트북 액정화면의 PPI 를 구해볼까요?

pixel 해상도: 1600 * 900
모니터 실제 크기: 34.4 cm * 19.3cm (자를 대고 육안으로 확인)
가로 인치 계산: 34.4 cm / 2.54 = 13.543 inch
세로 인치 계산: 19.3 cm / 2.54 = 7.59842 inch
가로 PPI: 1600 / 13.543 == 118.1422...
세로 PPI: 900 / 7.59842 == 118.4456...

Dell Studio 1557 노트북 사양에 보면 15.6" 모니터라고 했는데 13.543 * 13.543 + 7.59842 * 7.59842 = 183.412849 + 57.7359865 = 241.148835 가 나오고 그 값을 Sqrt(241.148835)로 하면 15.5289676... inch 라고 나오니... 육안으로 잰 것을 감안하면 그런데로 맞는 것 같습니다. ^^

어쨌든, 제 모니터에서는 118개의 pixel 이 화면에 보이는 경우 모니터 상에 자로 재보면 1 inch(2.54 cm) 가 나오는 것입니다.

그렇다면 WPF의 96 DIU 값은 96 DPI 로 설정된 제 액정에서는 96 개의 pixel로 표현이 됩니다. 제 모니터의 PPI 값이 118.1422... 이므로 96 개의 pixel 을 cm 로 환산해 보면 96 / 118.1422... * 2.54 == 2.06395... cm 가 나옵니다. 확인을 위해 96 DIU 인 선이 그어져 있는 액정 모니터에서 직접 자로 재보면 2 cm 가 나오는 것을 볼 수 있습니다.

자, 그럼 제 모니터를 기준으로 화면에 나오는 길이가 프린터로 나오는 길이와 동일하게 맞추고 싶다면 어떻게 해야 할까요? 간단합니다. 윈도우의 DPI 값을 118 로 맞춰주면 됩니다.

118 / 96 * 100 = 123.065... 가 나오므로, 다음과 같이 제어판에서 값을 조정해 주면 됩니다.

wpf_dpi_2.png

이렇게 조정하고, 다시 96 DIU 인 선을 액정 화면과 프린터 결과물에 대고 자로 재보면 동일하게 2.54 cm 정도가 나옵니다. 물론 대부분 96DPI 기본값을 수용하면서 PC에서 작업을 하지만,,, 아마도 디자이너들에게는 실물 크기 그대로 모니터에 나오는 것이 유용할 수 있겠지요.

(계산 결과들에 소수점 이하의 값이 포함되기 때문에 약간의 오차는 있을 수 있습니다.)




정리해 보면, 기존에는 2.54 cm의 선을 그으려면 우선 윈도우의 DPI 값을 알아내야 했습니다. 96DPI 로 설정된 경우에는 96 pixel로 선을 그으면 되었지만, 120DPI 에서는 120 pixel 로 개발자가 맞춰주어야 했습니다. 허나, DIU 값을 사용하고 있는 WPF 에서는 그냥 96 DIU 값을 설정해 놓으면 그 값 그대로 2.54 cm 의 선이 프린터로 출력되는 것입니다.

바로 이런 관계 때문에 WPF의 좌표 단위가 "디바이스 독립적인" 단위(DIU)라고 불리는 것입니다. 자... 이렇게 해서 DIU 에 대해서는 확실히 알게 되었고.

혹시, 시스템에 설정된 DPI 값을 프로그램으로 가져오는 방법이 있을까요? 이에 대해서는 다음의 글에서 잘 설명해 주고 있습니다.

Best way to get DPI value in WPF
; http://dzimchuk.net/blog/post/Best-way-to-get-DPI-value-in-WPF.aspx

몇몇 방법들을 소개해 주고 있는데, 여기서는 WPF 어셈블리 내에서 구할 수 있는 마지막 4번째 코드를 살펴보겠습니다.

public void TransformToPixels(Visual visual, double unitX, double unitY, out int pixelX, out int pixelY)
{
    Matrix matrix = PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice;
     
    pixelX = (int)(matrix.M11 * unitX);
    pixelY = (int)(matrix.M22 * unitY);
}

WPF에서는 개발자가 수작업으로 pixel 값을 맞춰줄 필요가 없어서인지 WPF 관련 어셈블리 내에서는 DPI 값 자체를 반환해 주는 방법은 없습니다. 단지, 반대로 DIU 단위가 몇 pixel로 표현이 되는지에 대해서 오히려 관심사가 되어버린 것입니다.

이렇게 DIU 값에 해당하는 pixel을 구하려면 DPI 비율을 알아내야 하는데 위의 코드에서 matrix.M11, matrix.M22 값으로 전달되는 것이 바로 그 비율입니다. 그리하여, 96 DPI 에서는 matrix.M11 == 1 이 되고, 120 DPI 에서는 matrix.M11 == 1.25 값이 나옵니다.

즉, 반대로 matrix.M11 * 96 을 하면 해당 윈도우 시스템의 DPI 값이 나옵니다.




위의 지식을 가지고, 지난 번에 작업했던 Bitmap 코드를 보면 RenderTargetBitmap 개체를 생성하는데 오류가 있다는 것을 확인할 수 있습니다.

==== 방법 2-1: Bitmap 그래픽으로 렌더링 ====

<ScrollViewer HorizontalScrollBarVisibility="Auto" Grid.Column="1" x:Name="scrollViewer1" >
    <Image Name="image1" Stretch="None" />
</ScrollViewer>

private void DrawVisual()
{
    DrawingVisual dv = new DrawingVisual();

    int size = 10;

    DrawingContext dc = dv.RenderOpen();
    Pen pen = new Pen(Brushes.Black, 1.0);

    Random rand = new Random();

    double x1 = 0;
    double y1 = 0;

    for (int i = 0; i < 50000; i++)
    {
        x1 = (i % 250) * size;
        y1 = (i / 250) * size;

        dc.DrawRectangle(null, pen, new Rect(x1, y1, size, size));
    }

    dc.Close();

    RenderTargetBitmap rtb = new RenderTargetBitmap(size * 250, size * 200, 96, 96, PixelFormats.Pbgra32);
    rtb.Render(dv);
    image1.Source = rtb;
}

4개의 인자 값이 요구되는 원래 단위는 다음과 같습니다.

size * 250: int pixelWidth
size * 200: int pixelHeight
첫번째 96: dpiX
두번째 96: dpiY

결국 위의 값들은 96 DPI 윈도우 설정에서는 들어맞지만, 120 DPI 로 설정된 경우에는 모든 값들이 유효하지 않게 됩니다. 따라서, 위의 코드는 다음과 같이 바뀌어야 합니다.

Size size = GetPixelSize(size * 250, size * 200);
var dpiValue = GetDPI();
            
RenderTargetBitmap rtb = new RenderTargetBitmap(
    (int)size.Width, (int)size.Height, dpiValue.Item1, dpiValue.Item2, PixelFormats.Pbgra32);

private Size GetPixelSize(int diuX, int diuY)
{
    int width, height;

    TransformToPixels(this, diuX, diuY, out width, out height);
    return new Size(width, height);
}

private Tuple<int, int> GetDPI()
{
    Matrix matrix = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice;

    return new Tuple<int, int>((int)matrix.M11 * 96, (int)matrix.M22 * 96);
}

public void TransformToPixels(Visual visual, double unitX, double unitY, out int pixelX, out int pixelY)
{
    Matrix matrix = PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice;

    pixelX = (int)(matrix.M11 * unitX);
    pixelY = (int)(matrix.M22 * unitY);
}




그 다음, 왜 WPF 에서 선을 그은 경우 뚜렷하지 않고 번져 보이게 되는지 알아보겠습니다.

전에도 설명드렸지만 이에 대해서는 다음의 글에서 잘 설명되고 있습니다.

Draw lines excactly on physical device pixels
; http://www.wpftutorial.net/DrawOnPhysicalDevicePixels.html

그래도, 이번 글에서 쓴 내용을 읽고 나서 위의 글을 읽어보시면 더욱 이해가 잘 되실 것입니다. ^^

그럼, 정리를 해볼까요?

우선, WPF 의 DIU 단위가 윈도우의 DPI 설정에 따라 1 pixel 로도 될 수 있고 1.25 pixel 로도 될 수 있기 때문에 번져 보이는 것은 당연합니다. 1.25 라는 pixel 은 없기 때문인데요. 개발자는 프린터에 0.010416... inch 의 굵기로 선을 그리고 싶어서 WPF 에서 1 DIU 값을 사용하지만 96 DPI 윈도우에서는 1pixel 로 정확하게 선을 표현할 수 있다고 해도 120 DPI 윈도우에서는 1.25 pixel을 화면에 정확하게 그릴 수가 없게 되는 것입니다. WPF 에서는, 어쩔 수 없이 이렇게 double 형식의 pixel 값을 인정해야만 합니다.

그렇다면, 우리가 흔히 쓰는 96 DPI 윈도우에서는 1 DIU 인 선이 정확하게 1 pixel 로 나와서 선이 그어져야 하지 않았을까요? 96 DPI 에서도 그렇게 나오지 않는 이유는, 1 pixel 의 시작 지점을 물리 pixel 과 일치시키지 않고 아래의 그림과 같이 2개의 pixel 에 걸쳐서 0.5 pixel 로 나눠서 표현했기 때문입니다.

[그림 출처: http://www.wpftutorial.net/DrawOnPhysicalDevicePixels.html]
wpf_dpi_3.png

결국, (2,2) ~ (6,2) DIU 값을 사용해서 WPF 에서 선을 그리면 논리적으로 (1.5,1.5) ~ (6.5, 2.5) 의 선을 나타내고, 화면에서는 이를 2개의 pixel로 번져 보이도록 표현하게 된 것입니다. 게다가 물리적인 2개의 pixel 로는 번짐 효과를 낼 수 없기 때문에 이를 위해 WPF 에서는 컬러 값을 조정해서 표현합니다. 구체적인 것은 다음의 Subpixel Rendering 설명을 참고하세요.

Subpixel rendering
; http://en.wikipedia.org/wiki/Subpixel_rendering

그런데, 다시 질문이 이어집니다. 그렇다면 2 DIU 값으로 지정하면 정확하게 2pixel 값을 꽉 채우게 되므로 선이 선명하게 표현되어야 할 텐데 종종 그렇지 않은 경우가 있는 것은 왜일까요? 이유는, 선이 그려지는 UIElement 의 위치부터 이미 double 값으로 틀어져 있기 때문에 2pixel 로 Pen 을 지정한 선을 긋는다 해도 연쇄적으로 번져 보이게 되는 것입니다.

자... 이에 대해서 눈으로 직접 테스트 해볼까요?

예제로 다음과 같이 XAML 을 구성하고,

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="251" Width="419" Loaded="Window_Loaded">
    <Grid Name="gridView">
        <Canvas Height="181" HorizontalAlignment="Left" 
                Margin="12,12,0,0" Name="canvas1" VerticalAlignment="Top" Width="294">
            <Line X1="10" Y1="10" X2="50" Y2="10" Stroke="Black" StrokeThickness="1" />
        </Canvas>

        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="312,12,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
    </Grid>
</Window>

button1_Click 이벤트 핸들러에서 각 UIElement 의 Pixel 좌표를 계산해 보겠습니다.

private void button1_Click(object sender, RoutedEventArgs e)
{
    Point pt = this.gridView.TransformToAncestor(this).Transform(new Point(0, 0));
    System.Diagnostics.Trace.WriteLine("Grid: " + pt);

    pt = this.canvas1.TransformToAncestor(this).Transform(new Point(0, 0));
    System.Diagnostics.Trace.WriteLine("Canvas: " + pt);
}

다음은 실행한 결과 및 좌표값입니다.

wpf_dpi_4.png

Grid: 0,0 
Canvas: 12,12 

WPF의 <Window /> 요소 자체는 윈도우 운영체제내에서는 Pixel 좌표로 화면에 뜨기 때문에 이에 꽉 차게 자리잡고 있는 <Grid /> 역시 pixel 좌표 상으로 (0, 0)으로 정확하게 위치하고 있습니다. 부모 Grid 가 정수 값의 pixel 좌표를 갖기 때문에 96 DPI 인 제 윈도우 시스템에서 Canvas 역시 (12,12) 의 pixel 좌표에 정확하게 일치하고 있습니다.

그런데, Canvas 내부에 그려진 선은 Pen 의 두께가 1 DIU 로 지정되었고 위에서 설명한 데로 2 pixel 에 걸쳐서 나오기 때문에 번져보이게 됩니다. 이론대로라면 이 상태에서 StrokeThickness 값을 2로 주면, 번짐 현상이 사라져야 하는데요. 실제로 해보시면 아래와 같이 예상대로 뚜렷한 2 pixel 의 선으로 나타납니다.

wpf_dpi_5.png

자, 그럼 다시 Line 값의 Y1/Y2 값을 double형으로 주어보면 어떻게 될까요?

<Line Name="line1" X1="10" Y1="10.5" X2="50" Y2="10.5" Stroke="Black" StrokeThickness="2" />

예상한 바와 같이, 1pixel 은 꽉 차게 표현되므로 뚜렷하게 나타나고 나머지 1pixel 은 0.5 pixel 씩 걸치게 되므로 희미하게 처리됩니다.

wpf_dpi_6.png

그렇다면 주의해서 언제나 정수형의 좌표 체계를 쓰면 그런데로 선을 뚜렷하게 표현할 수 있을 것 같은데, 현실은 그렇지 않습니다. 이전에도 설명한 것처럼 그것은 96 DPI 일 때의 가정이고, 윈도우 사용자가 임의의 값으로 DPI 값을 지정한 경우에는 어쩔 수 없이 double 형으로 pixel 값 대응이 이뤄질 수 밖에 없습니다. 그 외에도, Layout 컨트롤을 사용하는 경우에도 double 형 좌표값이 나올 수 있습니다.

예를 들어, 다음과 같이 Width == 419 인 윈도우에 2 개의 컬럼을 갖는 Grid를 정의한다면,

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="251" Width="419" Loaded="Window_Loaded">
    <Grid Name="gridView">
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
    </Grid>
</Window>

0번째 컬럼은 pixel 0 에서 시작하겠지만, 1번째 컬럼은 419 / 2 = 209.5 의 값을 가져서 어쩔 수 없이 double 형 값을 가지게 됩니다.

그렇다면, 그럴 때 마다 매번 Line 의 Y1/Y2 값을 double 형의 값을 상쇄시키는 값으로 지정해 주어야 하는 것일까요?

다행히, 이를 위해서 WPF 에서는 SnapsToDevicePixels 라는 속성을 제공해 주고 있는데 이 값을 True 로 하면 pixel 에 일치하게 값을 보정해 줍니다. 예를 들어, 위의 Line 에서는 다음과 같이 설정해 주면 다시 뚜렷한 2 pixel 의 선을 볼 수 있습니다.

<Line Name="line1" X1="10" Y1="10.5" X2="50" Y2="10.5" 
    Stroke="Black" StrokeThickness="2" SnapsToDevicePixels="True" />




SnapsToDevicePixels 과 UseLayoutRounding 속성의 차이는 뭘까요? 2가지 다 비슷한 의미로 사용되는데, 사실 주요한 차이점들이 있습니다.

  1. SnapsToDevicePixels 는 Silverlight 에서 지원되지 않는다.
  2. UseLayoutRounding 은 WPF 4.0 이상에서 지원되고 Silverlight 에서는 3.0 부터 지원된다.
  3. UseLayoutRounding 은 Layout 정렬 단계에서 픽셀값을 보정하고, SnapsToDevicePixels 은 Rendering 단계에서 보정한다.

1번과 2번은 그 자체가 설명이 되므로 생략하고, 3번의 차이점을 알아보기 위해 직접 환경을 만들어서 테스트를 해보겠습니다.

Layout 단계는 WPF 에서 다시 Measure/Arrange 단계로 나뉘는 데, 사용자 정의 Panel 을 만들면 UseLayoutRounding 값이 어떻게 적용되는지를 확인해 볼 수 있습니다. 이를 위해 다음과 같이 임의의 Panel을 만들고,

// 책 "Programming Windows Phone 7" 에 포함된 SingleCellGrid 소스 코드

public class MyPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        Size compositeSize = new Size();
        foreach (UIElement child in Children)
        {
            child.Measure(availableSize);
            compositeSize.Width = Math.Max(compositeSize.Width,
            child.DesiredSize.Width);
            compositeSize.Height = Math.Max(compositeSize.Height,
            child.DesiredSize.Height);
        }

        return compositeSize;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement child in Children)
        {
            child.Arrange(new Rect(new Point(), finalSize));
        }

        return base.ArrangeOverride(finalSize);
    }
}

이 패널에 다음과 같이 Rectangle 요소를 넣어보겠습니다.

<Window ...[생략]...>
    <my:MyPanel>
        <Rectangle UseLayoutRounding="False" Name="rect1" StrokeThickness="2"
            Width="45.5" Height="10.5" Margin="10" Stroke="Black" />
    </my:MyPanel>
</Window>

이제 Visual Studio 에서 디버깅 모드로 시작한 후, ArrangeOverride 에서 child.DesiredSize == {65.5, 30.5} 값으로 나오는 것을 확인할 수 있고, 소수점으로 인해 Rectangle 요소의 선은 번져서 나옵니다. 반면, 이 상태에서 UseLayoutRounding 값을 다시 "True" 로 바꾸면 child.DesiredSize == {66, 30} 으로 바뀌어 사각형의 외곽선은 다시 뚜렷하게 바뀝니다. 이처럼, UseLayoutRounding 은 해당 FrameworkElement 가 가지게 되는 영역 값에 소수점이 있다면 정수로 바꾸고 Pixel 정렬이 되도록 해주는 역할을 합니다.

UseLayoutRounding 속성이 Layout 단계에서만 효과를 발휘하는 이런 성질로 인해, MSDN 도움말에는 다음과 같은 문구가 나옵니다.

FrameworkElement.UseLayoutRounding Property 
; http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.uselayoutrounding

You should set UseLayoutRounding to true on the root element. The layout system adds child coordinates to the parent coordinates; therefore, if the parent coordinates are not on a pixel boundary, the child coordinates are also not on a pixel boundary. If UseLayoutRounding cannot be set at the root, set SnapsToDevicePixels on the child to obtain the effect that you want.


이를 테스트 하는 방법은 간단합니다. 다음과 같이 MyPanel 에 Margin 값을 소수점으로 주면 Rectangle 에 아무리 UseLayoutRounding 값이 True 로 설정되었다고 해도 다시 Rectangle 의 선은 번지게 됩니다.

<Window ...[생략]...>
    <my:MyPanel Margin="0.5 0.5 0.5 0.5">
        <Rectangle UseLayoutRounding="True" Name="rect1" StrokeThickness="2"
            Width="45.5" Margin="10" Height="10.5" Stroke="Black" />
    </my:MyPanel>
</Window>

이를 방지하기 위해서는, 가능한 최상단 요소(위의 예제에서는 Window 개체)에 UseLayoutRounding="True" 속성을 지정해 줄 것을 권장합니다. 그래서 다음과 같이 바꿔주면 다시 뚜렷한 선처리를 확인할 수 있습니다. (물론, 아래의 예제에서는 MyPanel 에 UseLayoutRounding="True" 값을 주어도 됩니다.)

<Window ...[생략]... UseLayoutRounding="True" >
    <my:MyPanel Margin="0.5 0.5 0.5 0.5" >
        <Rectangle Name="rect1" StrokeThickness="2"
            Width="45.5" Margin="10" Height="10.5" Stroke="Black" />
    </my:MyPanel>
</Window>

MSDN 문서에서는, 만약 UseLayoutRounding 속성을 최상위 개체에 사용할 수 없는 경우에 대해서도 언급하고 있는데, 그런 경우에는 SnapToDevicePixels 를 지정해 주라고 씌여 있습니다.

예를 들어, 다음과 같은 환경에서는 Rectangle 개체를 포함하는 부모 요소가 소수점을 포함하는 위치 값을 가지고 있으면서 그 요소를 임의로 변경할 수 없는 상황에서는 다음과 같이 SnapsToDevicePixels 값을 설정해야 합니다.

<Window ...[생략]...>
    <my:MyPanel Margin="0.2 0.2 0.2 0.2" >
        <Rectangle Name="rect1" StrokeThickness="2" SnapsToDevicePixels="True"
            Width="45.5" Margin="10" Height="10.5" Stroke="Black" />
    </my:MyPanel>
</Window>

SnapsToDevicePixels 속성은 Layout 단계에서 값을 보정하지 않고 실제 Rendering 시에 값을 보정해 주기 때문에 내부의 요소에 대한 누적값이 소수점인 경우에도 정상적으로 값을 보정해 주기 때문입니다.

문제는, SnapsToDevicePixels 처리가 WPF 에서 그다지 일률적으로 적용되지 않는다는 데 있습니다. 사실 System.Windows.Shapes.Shape 에 포함된 Ellipse, Line, Path, Polygon, Polyline, Rectangle 중에서 요소 내에 소수점을 포함하는 경우 SnapsToDevicePixels 이 적용되는 경우는 Rectangl 단 한 가지에 불과합니다. Ellipse 는 태생 자체가 타원을 표현해야 하기 때문에 점과 점이 곡선으로 부드럽게 연결되는 것을 표현하기 위해 "anti-aliasing" 이 처리되므로 어쩔 수 없고, 그 외의 Shape 클래스들은 내부적으로 소수점 좌표를 포함하는 경우 SnapsToDevicePixels 값에 상관없이 "anti-aliasing" 처리가 됩니다. 예를 들어, Path의 경우 다음과 같이 내부적으로 RectangleGeometry를 포함하더라도 그 값이 소수점 좌표를 포함하면 SnapsToDevicePixels (또는 UseLayoutRounding) 속성을 사용하더라도 "anti-aliasing" 처리가 됩니다.

<Path Stroke="Black" StrokeThickness="2" SnapsToDevicePixels="True" >
    <Path.Data>
        <RectangleGeometry Rect="10.5 10.5 20.5 20.5" />
    </Path.Data>
</Path>

엄밀히 따지면, 위의 경우를 예로 들어'SnapsToDevicePixels 처리가 일률적으로 적용되지 않는다'라는 것은 올바른 표현은 아닙니다. 왜냐하면 Path 요소의 목적 자체가 임의의 선을 긋는 것이므로 Ellipse 와 별다른 점이 없기 때문에 Anti-aliasing 으로부터 자유로울수는 없습니다. 물론, RectangleGeometry 요소의 의미가 단순 사각형을 의미하기는 하지만 Rectangle 요소와는 달리 Path 내부에서 AA 를 제거하도록 구성한다는 규칙을 두기에는 애매할 것이기 때문입니다.

따라서, AA 없는 사각형을 그리고 싶다면 Rectangle 요소를 사용해야 합니다. 그렇지 않고, Line, Path, Polyline, RectangleGeometry 등의 요소를 사용하게 되면 AA 없는 결과물을 얻기가 쉽지 않습니다.

또한, Canvas 상에 Shape 을 나타낼 때에는 Path 를 제외한 요소들에 대해서는 정상적으로 SnapsToDevicePixels 속성값이 적용되는 것을 확인할 수 있습니다.

정리하면, 만약 여러분들이 "anti-aliasing" 처리가 되지 않은 뚜렷한 선 처리를 원한다면 가장 최상위 클래스에 UseLayoutRounding="True" 값을 주고 그 이하의 요소들에서는 소수점 좌표를 사용하지 않아야만 원하는 목적을 이룰 수 있습니다.




마지막으로, 사용자가 직접 라인을 그리는 경우에 "anti-aliasing" 이 되지 않도록 처리하는 방법을 설명해 보겠습니다. 이 방법이 필요한 이유는 UseLayoutRounding 속성은 FrameworkElement 수준에서 제공이 되고, SnapsToDevicePixels 속성은 UIElement 수준에서 제공되므로 사용자가 직접 DrawingVisual 개체를 사용해서 그리는 값에 대해서는 적용되지 않기 때문입니다.

예를 들어서, 다음과 같이 DrawingVisual 개체를 이용하여 StackPanel 의 Background 에 선을 그린 경우,

=== XAML ===
<StackPanel Name="stackPanel" Width="450" Orientation="Horizontal">

=== XAML.cs ===
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();

drawingContext.DrawRectangle(null, new Pen(Brushes.Black, 2), new Rect(0, 0, 60, 60));

drawingContext.Close();

VisualBrush brush = new VisualBrush(drawingVisual);
brush.Stretch = Stretch.None;
stackPanel.Background = brush;

분명히, DrawRectangle 의 값으로 정수형 좌표를(0, 0, 60, 60) 설정해도 StackPanel 을 포함하는 윈도우의 크기가 바뀌면 "anti-aliasing" 처리가 되는 것을 확인할 수 있습니다.

이런 경우에는, 다음과 같이 그래픽 영역이 그려질 처음과 끝의 영역에 대해 정수값으로 가이드라인을 지정해 주면 "anti-aliasing" 처리가 사라져서 선이 뚜렷하게 그어집니다.

DrawingVisual drawingVisual = new DrawingVisual();

Point ptStart = this.stackPanel.TransformToAncestor(this).Transform(new Point(0, 0));
Size size = this.stackPanel.DesiredSize;
Point ptEnd = this.stackPanel.TransformToAncestor(this).Transform(new Point(size.Width, size.Height));

GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(ptStart.X);
guidelines.GuidelinesX.Add(ptEnd.X);
guidelines.GuidelinesY.Add(ptStart.Y);
guidelines.GuidelinesY.Add(ptEnd.Y);

DrawingContext drawingContext = drawingVisual.RenderOpen();

drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawRectangle(null, new Pen(Brushes.Black, 2), new Rect(0, 0, 60, 60));
drawingContext.Pop();

drawingContext.Close();

위의 코드에서 ptStart.X/Y, ptEnd.X/Y 는 소수점을 포함하고 있으면 안됩니다. 만약 다음과 같이 Margin 이 설정되어 소수점을 포함하게 되거나, 기타 레이아웃 관련 컨트롤의 자동 크기 조정이 되어 소수점이 적용된다면 필히 UseLayoutRounding="True"값을 주어서 정수 영역이 반환되도록 해주어야 합니다.

<StackPanel Name="stackPanel" Width="450" Orientation="Horizontal" Margin="0.2 0.2 0.2 0.2"
    UseLayoutRounding="True" >




휴~~~, 드디어 기나긴 여정을 마쳤습니다. 여기까지 쓰고 나니, 이제서야 저도 WPF 에서의 그래픽 좌표 단위와 Anti-aliased Rendering 에 대한 전반적인 체계가 이해가 됩니다.

어떠세요? 그런데로 재미있었지요. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 5/9/2012 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2020-08-13 07시45분
[오루라이] 감사합니다 도움이 많이 되었어요!
[손님]

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12438정성태12/2/202013오류 유형: 688. .Visual C++ - Error C2011 'sockaddr': 'struct' type redefinition
12437정성태12/1/202041VS.NET IDE: 155. pfx의 암호 키 파일을 Visual Studio 없이 등록하는 방법
12436정성태12/1/202012오류 유형: 687. .NET Core 2.2 빌드 - error MSB4018: The "RazorTagHelper" task failed unexpectedly.
12435정성태12/1/202045Windows: 181. 윈도우 환경에서 클라이언트 소켓의 최대 접속 수 (4) - ReuseUnicastPort를 이용한 포트 고갈 문제 해결파일 다운로드1
12434정성태12/2/202079Windows: 180. C# - dynamicport 값의 범위를 알아내는 방법
12433정성태12/1/202062Windows: 179. 윈도우 환경에서 클라이언트 소켓의 최대 접속 수 (3) - SO_PORT_SCALABILITY파일 다운로드1
12432정성태11/30/2020142Windows: 178. 윈도우 환경에서 클라이언트 소켓의 최대 접속 수 (2) - SO_REUSEADDR [1]파일 다운로드1
12431정성태11/27/202074.NET Framework: 976. UnmanagedCallersOnly + C# 9.0 함수 포인터 사용 시 x86 빌드에서 오동작하는 문제파일 다운로드1
12430정성태11/27/202027오류 유형: 686. Ubuntu - E: The repository 'cdrom://...' does not have a Release file.
12429정성태12/2/202066디버깅 기술: 175. windbg - 특정 Win32 API에서 BP가 안 걸리는 경우
12428정성태11/25/202043VS.NET IDE: 154. Visual Studio - .NET Core App 실행 시 dotnet.exe 실행 화면만 나오는 문제
12427정성태11/25/2020123.NET Framework: 975. .NET Core를 직접 호스팅해 (runtimeconfig.json 없이) EXE만 배포해 실행파일 다운로드1
12426정성태11/24/202035오류 유형: 685. WinDbg Preview - error InitTypeRead
12425정성태11/24/202028VC++: 141. Visual C++ - "Treat Warnings As Errors" 옵션이 꺼져 있는데도 일부 경고가 에러 처리되는 경우
12424정성태11/24/202068VC++: 140. C++의 연산자 동의어(operator synonyms), 대체 토큰
12423정성태11/22/2020117.NET Framework: 974. C# 9.0 - (16) 제약 조건이 없는 형식 매개변수 주석(Unconstrained type parameter annotations)파일 다운로드1
12422정성태11/21/202078.NET Framework: 973. .NET 5, .NET Framework에서만 허용하는 UnmanagedCallersOnly 사용예파일 다운로드1
12421정성태11/23/202081.NET Framework: 972. DNNE가 출력한 NE DLL을 직접 생성하는 방법파일 다운로드1
12420정성태11/19/202040오류 유형: 684. Visual C++ - MSIL .netmodule or module compiled with /GL found; restarting link with /LTCG; add /LTCG to the link command line to improve linker performance
12419정성태11/23/202099VC++: 139. Visual C++ - .NET Core의 nethost.lib와 정적 링크파일 다운로드1
12418정성태11/19/202038오류 유형: 683. Visual C++ - error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MT_StaticRelease' doesn't match value 'MDd_DynamicDebug'파일 다운로드1
12417정성태11/19/202041오류 유형: 682. Visual C++ - warning LNK4099: PDB '...pdb' was not found with '...lib(pch.obj)' or at '...pdb'; linking object as if no debug info
12416정성태11/19/202029오류 유형: 681. Visual C++ - error LNK2001: unresolved external symbol _CrtDbgReport
12415정성태11/19/202089.NET Framework: 971. UnmanagedCallersOnly 특성과 DNNE 사용파일 다운로드1
12414정성태11/20/2020127VC++: 138. x64 빌드에서 extern "C"가 아닌 경우 ___cdecl name mangling 적용 [4]파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...