VB :: Lecture & TIPs

[VB.NET] Windows Forms 프로젝트에서 High-DPI (고해상도) 화면에 대응하기.

2000 년대 까지는 수많은 디지털 기기들이 화면에 대한 큰 욕심을 내지 않았습니다. 그저 ‘문자를 보여 줄 수 있는’ 최소한의 낮은 해상도를 사용해오면서 원가 절감과 상용화된 기술 사용에 초점을 맞추면서 사용자는 편의적으로도, 심미적으로도 만족 할 수 없는 이미 익숙해진 사용자 경험에 만족할 수 밖에 없는 상황이었습니다.

하지만 2010 년, Apple 사 (社) 에서 사람의 시각을 기준으로 망막이 각 픽셀들을 인지 할 수 없다는 의미의 Retina Display (레티나 디스플레이) 라는 새로운 마케팅 용어를 선보임과 더불어 고밀도 디스플레이를 탑재한, 960 × 640 픽셀 크기의 해상도에 인치 당 픽셀 수 (PPI) 가 326 개로 300 이상의 PPI 를 지원하는 iPhone 4 제품을 공개했고, 실제로 그 선명도는 타사의 스마트폰과 비교가 되지 않을 정도로 깔끔하고 선명해 가독성도 높아졌을 뿐 아니라 미적인 면에서도 아름다워서, 이 시기를 기점으로 각 제조사들은 300 PPI 이상의 고해상도 디스플레이를 탑재한 제품들 (예 : Samsung Galaxy S5, S6, Nokia Lumia 920, 930 등) 을 너도나도 선보였고, 2016 년에 출시된 Microsoft 사의 Lumia 950 제품이나 Samsung Galaxy S6 제품의 경우에는 각각 564, 577 PPI 라는 경이로운 선명도를 가진 디스플레이를 탑재하여 시판되어 본격적으로 고밀도 기술과 고해상도 기술이 접목된 기기들이 상용화되기 시작했습니다.


어느 덧 모바일 쪽 제품군의 디스플레이 성능이 평준화되자, Apple 은 이러한 고해상도 디스플레이를 컴퓨터 계열의 제품에 탑재하는 것을 시도했는데요, 맥북에도 3K 에 가까운 (2880 × 1880) Retina Display 를 탑재하면서 처음으로 컴퓨터 화면을 선명하게 만드는 데 성공했고, 운영체제 UI 전체에 Anti-Aliasing 을 지원함으로써 macOS 특유의 서체 렌더링을 통해 장점을 극대화하였습니다.


Microsoft Windows 의 Hi-DPI 지원

Hi-DPI 를 완벽하게 지원하는 macOS 의 경우 Apple 이 시스템의 기반이 되는 커널 (Kernel) 을 여러번 리모델링하면서 기존 프로그램의 지원을 가차없이 중단함에 따라 Hi-DPI 를 제대로 지원하는 앱 (Application) 들이 많아져 현재는 이로 인한 큰 문제가 없습니다.

하지만, Windows 운영체제에서는 상황이 다릅니다. Windows 운영체제의 가장 큰 장점을 꼽자면 레거시 (Legacy) 프로그램 지원일 것입니다. 출시 된 지 20 년이 지난 옛날 게임인 스타크래프트가 공식적으로 최신 윈도우를 지원하게 된 1.18 패치 이전에도 Windows 10 에서 원활한 실행이 가능했던 사례가 있고, 2000 년도 때 유행했던 다수의 온라인 게임들을 최근에도 문제없이 실행이 가능한데요, 이는 Microsoft 사에서 하위 버전과의 호환성을 유지하려 부단히 노력한 결과물로 볼 수 있겠으나, 이러한 호환성 유지에 대한 Microsoft 의 기조로 인해 mac OS 에 비해 소프트웨어적 측면에서 Hi-DPI (고해상도 디스플레이) 의 도입이 더뎠습니다. 경쟁사의 MacBook 이 2012년 중반 2560 × 1600 (227 ppi) 해상도의 13 인치 모델이 출시되었을 때 Microsoft 사 (社) 는 첫 Surface Pro 제품 라인업을 공개했고, 비슷한 시기에 Surface Pro 첫 제품이 1920 x 1080 (208 PPI) 인치당 픽셀 수는 비슷하게 10.6 인치 모델로 발표되었지만, 소프트웨어적으로는 부족함이 많았습니다. 당시 대부분의 앱들이 고해상도를 지원하지 않아 자글거림 문제나 각 컨트롤들의 레이아웃이 깨지는 이슈가 존재했고, 이러한 문제점들이 사용자 경험과 직결되어 실제로 Windows 운영체제의 고해상도 지원에 대한 부정적인 피드백이나 해결책을 찾는 게시물들이 온라인 게시판이나, 커뮤니티 상에도 많이 올라오는 결과로 이어졌습니다.

이에, 이번 게시물에서는 Windows Forms 프로젝트의 고해상도 (High-DPI) 디스플레이 화면에 어떻게 대응할 수 있을지를 놓고 꾸준히 고민을 해 온 끝에 고안한 해결방법들을 소개하는 시간을 가져보도록 하겠습니다.


Hi-DPI Scaling (스케일링) 이란?

화면 속에서 x × y 크기인 객체를 xn × yn 크기로 확대하는 것을 ‘scaling (스케일링)’ 이라고 합니다. Windows 운영체제에서 기준 값으로 가지고 있는 수치는 100% (96 PPI) 이며, 화면 크기와 비례했을 때 해상도가 높은 경우 화면에 표시되는 앱이나 Form 내에 아이콘 등의 객체들이 작게 표시되는데 이를 n 배로 확대 시켜 알맞은 크기로 변경하여 가독성을 높이는 방식을 Windows 에서는 “Hi-DPI 스케일링” 으로 부릅니다.


작업 시 고안이 필요한 요소들

생성된 Form 의 AutoScaleMode 속성을 DPI 혹은 Font 로 선택합니다. 속성을 변경한 이후, Form 내에 배치해 둔 컨트롤들의 크기를 계산하여 적절한 값을 대입해줍니다. Designer View 에서 사용하는 모든 단위는 100% 비율인 96 DPI 를 기준으로 맞추어져 있으므로 배율에 비례하도록 값을 곱하여 확대합니다.

예를 들어 100% (96 DPI) 인 기준 환경에서, 너비 860 픽셀에 높이 675 픽셀로 Form 을 생성해 사용했다면, 고해상도 디스플레이에서 배율이 200% 로 설정되어 있다고 가정하면, 너비와 폭에 각각 200% 를 소수로 변환했을 때의 값인 2.0 을 곱하여 너비 1720, 높이 1350 픽셀로 size 속성에 값을 대입해야 추후 이보다 낮은 배율의 화면에서 실행하였을 때에도 폼의 구조가 손상되지 않습니다.

여기서 주의점은, 해상도와 픽셀 밀도가 상이한 여러가지 모델의 모니터 여러 대를 확장 모드로 놓고 사용할 경우 Visual Studio 를 실행해 작업하는 주모니터가 어떤 화면이냐에 따라서 속성값을 일일이 계산해보아야 하는 문제점이 발생합니다.

Form 의 구조, 다시말해 레이아웃이 의도치 않게 손상되는 문제를 예방하려면 모든 화면의 배율을 맞추거나 하나의 모니터만 사용하도록 디스플레이 설정을 변경하는 편이 좋습니다.


계산 문제를 쉽게 해결하기

이와 같이 크기 계산을 일일이 수행하여 작업해야 하므로 번거롭다면, Visual Studio 에서 제공하는 상단 팝업의 링크를 이용해 DPI 비율을 100% 로 낮추어 작업하는 것이 보다 간편할 수 있습니다. Windows High-DPI 화면에서 혹은 배율 설정 상태로 Visual Studio 를 실행해 Windows Forms 프로젝트에서 제공하는 Designer View 를 실행하면 아래와 같은 노란색 팝업이 표시됩니다.

DPI 를 강제적으로 바꿔 사용하는 방법보다 Visual Studio 를 DPI 스케일링 비인식모드로 재시작하는 링크만 클릭하면 Windows 설정 값에 상관없이 Visual Studio 만 단독적으로 기준치인 96 DPI 로 다운스케일링이 적용된 상태로 작업중이던 프로젝트 파일을 불러와줍니다. 물론, 이렇게 작업하시다가 기존 설정으로 되돌아가는 기능도 제공합니다.

이렇게 링크를 클릭해 다운스케일링이 적용된 상태에서는, 각 100% 배율의 표준값인 1.0 을 기준으로 스크린에 대한 비례 단위를 곱하는 것으로 복잡하게 계산할 필요가 없어집니다.

비례 단위배율DPI
1.0100%96
1.5150%192
2.0200%288


GUI Adjustment.vb

Imports System.Runtime.InteropServices

Module GUIAdjustment

#Region "Adjustment Controls"
    <Runtime.CompilerServices.Extension()>
    Public Function findAllChildren(ByRef startingContainer As System.Windows.Forms.Form) As List(Of System.Windows.Forms.Control)
        Dim Children As New List(Of System.Windows.Forms.Control)

        Dim oControl As System.Windows.Forms.Control
        For Each oControl In startingContainer.Controls
            Children.Add(oControl)
            If oControl.HasChildren Then
                Children.AddRange(oControl.findAllChildren())
            End If
        Next

        Return Children
    End Function

    <Runtime.CompilerServices.Extension()>
    Public Function findAllChildren(ByRef startingContainer As System.Windows.Forms.Control) As List(Of System.Windows.Forms.Control)
        Dim Children As New List(Of System.Windows.Forms.Control)

        If startingContainer.HasChildren = False Then
            Return Nothing
        Else
            Dim oControl As System.Windows.Forms.Control
            For Each oControl In startingContainer.Controls
                Children.Add(oControl)
                If oControl.HasChildren Then
                    Children.AddRange(oControl.findAllChildren)
                End If
            Next
        End If

        Return Children
    End Function

    Public Sub zoomForm(Form As Form, zoomFactor As Double, Refresh As Boolean)
        Dim g As Graphics = Form.CreateGraphics
        Dim dpi = g.DpiX.ToString
        Dim ratio = dpi / 96

        Form.Height *= zoomFactor
        Form.Width *= zoomFactor

        For Each c As Control In Form.findAllChildren
            c.Width *= zoomFactor
            c.Height *= zoomFactor

            c.Location = New Point(c.Location.X, c.Location.Y)

            Try
                c.Font = New System.Drawing.Font(c.Font.Name, c.Font.Size * zoomFactor, c.Font.Style, System.Drawing.GraphicsUnit.Point, CType(0, Byte))
            Catch ex As Exception

            End Try
        Next

        If Refresh Then Form.Refresh()
    End Sub
#End Region
End Module


MainForm.vb

Public Class MainForm
    Dim zoomfactor As Double = 1.0!

    Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        zoomForm(Me, zoomfactor, False)
    End Sub
End Class


설정 값과 디자인 환경과의 비례 관계

디자인 환경에서는 AutoScaleMode 속성을 DPI 값으로 설정하였을 경우 자동으로 AutoScaleDimension 이라는 숨겨진 속성에 값을 대입합니다. 이 때 현재 Visual Studio 가 실행중인 화면의 배율에 따라 값을 수정하며, 해당 값에 비례하여 Designer View 내에 배치된 모든 컨트롤들의 Size 속성의 값도 자동으로 변경됩니다.

어떠한 배율의 환경에서 작업이 이루어지든 상관없이 크기 값을 정확하게 지정하셨다면 이와 같은 방법으로 Designer View 와 Runtime 시에 각 객체의 크기가 자동으로 조정되는 것으로 이해할 수 있습니다.

이러한 방법 상 주의하여야 할 부분이 존재한다면 소수점 연산의 특성 상 최종적으로 계산된 컨트롤 위치에 대한 좌표가 미세하게 이동될 수 있다는 점입니다. Size 속성에 대입되는 값은 소수점 이하의 숫자를 지원하지 않는 정수 값만 대입할 수 있으므로, 컨트롤들을 지나치게 좁혀 세밀하게 배치한다면, 이러한 변화로 인하여 문제가 발생할 가능성이 높습니다.

따라서, 가장 바람직한 레이아웃 설계방법은 Dock, Anchor 속성을 효율적으로 이용하거나 각종 컨네이너 컨트롤 (Table Layout Panel, Stack Layout Panel 등) 을 활용하는 방식입니다.


앱 Runtime 설정 값 변경하기

Designer View 에서 문제를 파악하고, 수정한 후에는 Runtime 설정을 변경하여 최적의 상태로 실행되도록 환경을 조성해주는 절차가 필요합니다.

우선 현재 작업중인 프로젝트에서 [솔루션 탐색기] 창의 [My Project] 항목을 클릭하신 후 [Application] 탭 하위의 [View Windows Settings] 버튼을 클릭하면 [app.manifest] 파일이 프로젝트에 추가됩니다.

assembly 태그 하위에 아래와 같은 XML 마크업 코드에 대하여 주석 처리를 해제해줍니다.


app.manifest

  <!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
       DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need 
       to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should 
       also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->

  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
  </application>

최종적으로 프로젝트의 app.config 파일 내 <appSettings> 태그에 아래의 코드를 추가합니다. value 속성 값이 true 가 아닌 True 로 설정되어야 함에 유의합니다. 해당 코드 적용은 .NET Framework 4.7 이상의 버전을 Target Framework 로 지정하고 빌드 후 실행하여야만 합니다.


app.config

  <appSettings>
    <add key="EnableWindowsFormsHighDpiAutoResizing" value="True" />
  </appSettings>


위와 같이 작업할 수 없는 경우

만약 소스 코드를 사용하실 수 없거나, 컨트롤의 배치나 레이아웃 측면에서 유지보수가 곤란하도록 설계된 앱이라면, 반대로 DPI Awareness 를 사용하지 않도록 호환성 옵션을 맞추거나, 위의 Manifest 설정에서 DPI Awareness 속성과 관련된 모든 값을 False 로 변경하면 Windows 운영체제가 배율에 만족하도록 화면을 강제적으로 확대하여 표시하도록 할 수 있습니다.

이렇게 하면 앱 내의 텍스트나 사진, 이미지 등이 뿌옇게 흐려지는 부작용이 발생하지만 Windows 10, 2018년 4월 업데이트 (Build 1803) 이후 버전부터는 해당 문제를 완화하는 기능을 추가로 제공하여 앱 사용시 불편함이 없도록 구현할 수는 있습니다. 그러나, 기능에 한계가 있어 완벽하게 구현된다는 보증은 없으며, 렌더링에 필요한 리소스 소비량이 많아지므로 앱 구동성능에 영향이 있을 수 있습니다.

내용이 도움 되셨기를 바라며 감사합니다.

Leave a comment