Como escrever código WinForms que escala automaticamente as configurações de fonts e dpi do sistema?

Introdução: Há muitos comentários por aí que dizem “O WinForms não dimensiona automaticamente as configurações de DPI / fonte; mude para o WPF.” No entanto, penso que é baseado no .NET 1.1; Parece que eles realmente fizeram um bom trabalho ao implementar o auto-scaling no .NET 2.0. Pelo menos com base em nossas pesquisas e testes até agora. No entanto, se alguns de vocês souberem disso, adoraríamos saber de você. (Por favor, não se incomode em discutir que devemos mudar para o WPF … isso não é uma opção no momento.)

Questões:

Diretrizes de Design que identificamos até agora:

Veja a resposta do wiki da comunidade abaixo.

Algum desses incorretos ou inadequados? Quaisquer outras orientações que devemos adotar? Existem outros padrões que precisam ser evitados? Qualquer outra orientação sobre isso seria muito apreciada.

Controles que não suportam o dimensionamento corretamente:

  • Label com AutoSize = False e Font herdada. Definir explicitamente Font no controle para que ele apareça em negrito na janela Propriedades.
  • ListView coluna ListView não dimensionam. Substituir ScaleControl do ScaleControl para fazer isso em vez disso. Veja esta resposta
  • Propriedades de Panel1MinSize , Panel2MinSize e SplitterDistance SplitContainer
  • TextBox com MultiLine = True e Font herdados. Definir explicitamente Font no controle para que ele apareça em negrito na janela Propriedades.
  • ToolStripButton . No construtor do formulário:

    • Definir ToolStrip.AutoSize = False
    • Defina ToolStrip.ImageScalingSize acordo com CreateGraphics.DpiX e .DpiY
    • Set ToolStrip.AutoSize = True se necessário.

    Às vezes, o AutoSize pode ser deixado em True mas às vezes ele não é redimensionado sem essas etapas. Funciona sem alterações com o .NET Framework 4.5.2 e EnableWindowsFormsHighDpiAutoResizing .

  • Imagens de TreeView . Defina ImageList.ImageSize acordo com CreateGraphics.DpiX e .DpiY . Para StateImageList , funciona sem alterações com o .NET Framework 4.5.1 e EnableWindowsFormsHighDpiAutoResizing .
  • Tamanho do Form . Escala tamanho fixo Form é manualmente após a criação.

Diretrizes de design:

  • Todos os ContainerControls devem ser definidos para o mesmo AutoScaleMode = Font . (A fonte manipulará as alterações de DPI e as alterações na configuração de tamanho de fonte do sistema; o DPI só lidará com alterações de DPI, não com alterações na configuração de tamanho de fonte do sistema.)

  • Todos os ContainerControls também devem ser definidos com AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); , assumindo 96dpi (veja o próximo marcador). Isso é auto-adicionado pelo designer com base no DPI que você abre o designer em … mas faltava em muitos dos nossos arquivos de designer mais antigos. Talvez o Visual Studio .NET (a versão anterior ao VS 2005) não estivesse adicionando isso corretamente.

  • Faça todo o seu trabalho de designer em 96dpi (poderemos mudar para 120dpi; mas a sabedoria na internet diz para ficar com 96dpi; a experimentação está em ordem lá; por design, não importa, basta mudar a linha AutoScaleDimensions que o designer insere). Para configurar o Visual Studio para executar em um 96dpi virtual em uma canvas de alta resolução, localize seu arquivo .exe, clique com o botão direito do mouse para editar propriedades e, em Compatibilidade, selecione “Substituir comportamento de escala DPI alto. Escala executada por: Sistema”.

  • Certifique-se de nunca definir a fonte no nível do contêiner … apenas nos controles de folha. (Definir a fonte em um contêiner parece desativar o dimensionamento automático desse contêiner.)

  • NÃO use Anchor Right ou Bottom ancorado a um UserControl … seu posicionamento não será autoescalonado; em vez disso, solte um painel ou outro contêiner em seu UserControl e ancore seus outros controles nesse painel; faça com que o Painel use Dock Right ou Dock Bottom no seu UserControl.

  • Somente os controles nas listas Controls quando ResumeLayout no final do InitializeComponent é chamado serão auto-escalados … se você adicionar dinamicamente controles, então você precisa de SuspendLayout(); AutoScaleDimensions = new SizeF(6F, 13F); AutoScaleMode = AutoScaleMode.Font; ResumeLayout(); nesse controle antes de adicioná-lo. E seu posicionamento também precisará ser ajustado se você não estiver usando os modos Dock ou um Layout Manager como FlowLayoutPanel ou TableLayoutPanel .

  • As classs base derivadas de ContainerControl devem deixar AutoScaleMode definido como Inherit (o valor padrão definido na class ContainerControl ; mas NÃO o padrão definido pelo designer). Se você configurá-lo para qualquer outra coisa e, em seguida, sua class derivada tenta configurá-lo para fonte (como deveria), o ato de definir que a Font irá limpar a configuração de AutoScaleDimensions do designer, resultando em realmente desabilitar auto-escala ! (Esta diretriz combinada com a anterior significa que você nunca pode instanciar classs base em um designer … todas as classs precisam ser projetadas como classs base ou como classs folha!)

  • Evite usar Form.MaxSize estaticamente / no Designer. MinSize e MaxSize no formulário não dimensionam tanto quanto todo o resto. Portanto, se você fizer todo o seu trabalho em 96 dpi, quando em DPI mais alto, o MinSize não causará problemas, mas poderá não ser tão restritivo quanto o esperado, mas o MaxSize poderá limitar o dimensionamento do seu tamanho, o que pode causar problemas. Se você quiser MinSize == Size == MaxSize , não faça isso no Designer … faça isso em seu construtor ou substitua OnLoad … defina MinSize e MaxSize para o seu Tamanho adequadamente dimensionado.

  • Todos os controles em um determinado Panel ou Container devem usar ancoragem ou encaixe. Se você misturá-los, o dimensionamento automático feito por esse Panel muitas vezes se comportará mal de maneiras sutis e bizarras.

Minha experiência tem sido bastante diferente da atual resposta votada. Ao percorrer o código do .NET framework e ler atentamente o código-fonte de referência, concluí que tudo está em vigor para que o escalonamento automático funcione, e havia apenas um problema sutil em algum lugar bagunçando tudo. Isso acabou por ser verdade.

Se você criar um layout adequadamente reflutável / de tamanho automático, quase tudo funcionará exatamente como deveria, automaticamente, com as configurações padrão usadas pelo Visual Studio (ou seja, AutoSizeMode = Font no formulário pai e Herdar em todo o resto).

A única pegadinha é se você tiver definido a propriedade Font no formulário do designer. O código gerado classificará as atribuições em ordem alfabética, o que significa que AutoScaleDimensions será atribuído antes de Font . Infelizmente, isso interrompe completamente a lógica de dimensionamento automático do WinForms.

A correção é simples embora. Ou não defina a propriedade Font no designer (configure-a em seu construtor de formulário) ou reordene manualmente essas atribuições (mas você terá que continuar fazendo isso sempre que editar o formulário no designer). Voila, escala quase perfeita e totalmente automática com o mínimo de problemas. Até mesmo os tamanhos dos formulários são dimensionados corretamente.


Vou listar os problemas conhecidos aqui como eu os encontro:

  • O TableLayoutPanel nested calcula as margens de controle incorretamente . Não há trabalho conhecido em torno de evitar margens e preenchimentos completamente – ou evitar painéis de layout de tabela nesteds.

Direcione seu Aplicativo para o .Net Framework 4.7 e execute-o no Windows 10 v1703 (Criador Atualização Build 15063). Com o .Net 4.7 no Windows 10 (v1703), o MS fez muitas melhorias no DPI .

Começando com o .NET Framework 4.7, o Windows Forms inclui aprimoramentos para cenários comuns de alta DPI e DPI dynamic. Esses incluem:

  • Aprimoramentos no dimensionamento e no layout de vários controles do Windows Forms, como o controle MonthCalendar e o controle CheckedListBox.

  • Escalonamento de passagem única. No .NET Framework 4.6 e nas versões anteriores, o escalonamento era executado por meio de várias passagens, o que fazia com que alguns controles fossem dimensionados mais do que o necessário.

  • Suporte para cenários de DPI dynamic nos quais o usuário altera o DPI ou o fator de escala após o lançamento de um aplicativo do Windows Forms.

Para apoiá-lo, adicione um manifesto de aplicativo ao seu aplicativo e sinalize que seu aplicativo suporta o Windows 10:

       

Em seguida, adicione um app.config e declare o aplicativo Per Monitor Aware. Isso agora é feito em app.config e não no manifesto como antes!

    

Este PerMonitorV2 é novo desde a atualização do Windows 10 Creators:

DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2

Também conhecido como Per Monitor v2. Um avanço em relação ao modo de reconhecimento de DPI por monitor original, que permite que os aplicativos acessem novos comportamentos de dimensionamento relacionados a DPI em uma base de janela de nível superior.

  • Notificações de alteração do DPI da janela filho – Em contextos do Por Monitor v2, toda a tree de janelas é notificada sobre quaisquer alterações de DPI que ocorram.

  • Escala da área não cliente – Todas as janelas terão automaticamente sua área não cliente desenhada de maneira sensível a DPI. Chamadas para EnableNonClientDpiScaling são desnecessárias.

  • S calagem de menus Win32 – Todos os menus NTUSER criados em contextos Per Monitor v2 serão redimensionados de maneira por monitor.

  • Escalonamento de checkbox de diálogo – As checkboxs de diálogo do Win32 criadas nos contextos do Monitor v2 responderão automaticamente às alterações de DPI.

  • Melhor dimensionamento de controles comctl32 – Vários controles comctl32 melhoraram o comportamento de dimensionamento de DPI em contextos do Monitor v2.

  • Melhor comportamento de temas – As alças UxTheme abertas no contexto de uma janela Por Monitor v2 operarão em termos do DPI associado a essa janela.

Agora você pode se inscrever em três novos events para ser notificado sobre alterações de DPI:

  • Control.DpiChangedAfterParent , que é triggersdo Ocorre quando a configuração de DPI de um controle é alterada programaticamente depois que um evento de alteração de DPI para seu controle pai ou formulário ocorreu.

  • Control.DpiChangedBeforeParent , que é acionado quando a configuração de DPI de um controle é alterada programaticamente antes que um evento de alteração de DPI para seu controle pai ou formulário tenha ocorrido.

  • Form.DpiChanged , que é acionado quando a configuração de DPI é alterada no dispositivo de exibição em que o formulário é exibido atualmente.

Você também tem 3 methods auxiliares sobre manipulação / dimensionamento de DPI:

  • Control.LogicalToDeviceUnits , que converte um valor de lógico para pixels do dispositivo.

  • Control.ScaleBitmapLogicalToDevice , que dimensiona uma imagem de bitmap para o DPI lógico de um dispositivo.

  • Control.DeviceDpi , que retorna o DPI para o dispositivo atual.

Se você ainda encontrar problemas, poderá optar por sair das melhorias de DPI por meio das inputs app.config .

Se você não tiver access ao código-fonte, poderá acessar as propriedades do aplicativo no Windows Explorer, ir até a compatibilidade e selecionar System (Enhanced)

insira a descrição da imagem aqui

que ativa o dimensionamento de GDI para melhorar também o tratamento de DPI:

Para aplicativos que são baseados em GDI, o Windows agora pode dimensionar DPI em uma base por monitor. Isso significa que esses aplicativos, magicamente, se tornarão conscientes de DPI por monitor.

Execute todas essas etapas e você deverá obter uma melhor experiência DPI para aplicativos WinForms. Mas lembre-se, você precisa direcionar seu aplicativo para .net 4.7 e precisa pelo menos do Windows 10 Build 15063 (Atualização de criadores). No próximo Windows 10 Update 1709, poderemos obter mais melhorias.

Um guia que escrevi no trabalho:

O WPF funciona em ‘unidades independentes de dispositivo’, o que significa que todos os controles são dimensionados perfeitamente para canvass de alta resolução. No WinForms, é preciso mais cuidado.

O WinForms funciona em pixels. O texto será dimensionado de acordo com o dpi do sistema, mas geralmente será cortado por um controle sem escala. Para evitar tais problemas, você deve evitar o tamanho e o posicionamento explícitos. Siga estas regras:

  1. Onde quer que você o encontre (labels, botões, painéis) defina a propriedade AutoSize como True.
  2. Para layout, use FlowLayoutPanel (a la WPF StackPanel) e TableLayoutPanel (a la WPF Grid) para layout, em vez de vanilla Panel.
  3. Se você estiver desenvolvendo em uma máquina de alta dpi, o designer do Visual Studio pode ser uma frustração. Quando você define AutoSize = True, ele resizeá o controle para a canvas. Se o controle tiver AutoSizeMode = GrowOnly, ele permanecerá nesse tamanho para pessoas em dpi normal, ou seja. ser maior que o esperado. Para corrigir isso, abra o designer em um computador com dpi normal e clique com o botão direito do mouse, redefina.

Eu achei muito difícil fazer com que o WinForms funcionasse bem com alta DPI. Então, eu escrevi um método VB.NET para replace o comportamento do formulário:

 Public Shared Sub ScaleForm(WindowsForm As System.Windows.Forms.Form) Using g As System.Drawing.Graphics = WindowsForm.CreateGraphics Dim sngScaleFactor As Single = 1 Dim sngFontFactor As Single = 1 If g.DpiX > 96 Then sngScaleFactor = g.DpiX / 96 'sngFontFactor = 96 / g.DpiY End If If WindowsForm.AutoScaleDimensions = WindowsForm.CurrentAutoScaleDimensions Then 'ucWindowsFormHost.ScaleControl(WindowsForm, sngFontFactor) WindowsForm.Scale(sngScaleFactor) End If End Using End Sub 

Além das âncoras não funcionarem muito bem: eu iria mais longe e diria que o posicionamento exato (também conhecido como usar a propriedade Location) não funciona muito bem com o dimensionamento da fonte. Eu tive que resolver esse problema em dois projetos diferentes. Em ambos, tivemos que converter o posicionamento de todos os controles do WinForms para o uso de TableLayoutPanel e FlowLayoutPanel. O uso da propriedade Dock (geralmente definida como Fill) dentro do TableLayoutPanel funciona muito bem e é dimensionada com o DPI da fonte do sistema.

Recentemente, deparei com esse problema, especialmente em combinação com o Visual Studio reescalonando quando o editor é aberto em um sistema de alta resolução. Eu achei melhor manter AutoScaleMode = Font , mas para definir a fonte de formulários para a fonte padrão, mas especificando o tamanho em pixel , não ponto, ou seja: Font = MS Sans; 11px Font = MS Sans; 11px . No código, redefino a fonte para o padrão: Font = SystemFonts.DefaultFont e está tudo bem.

Apenas meus dois centavos. Eu pensei em compartilhar, porque “manter AutoScaleMode = Font” e “Definir tamanho da fonte em pixel para o Designer” era algo que eu não encontrei na internet.

Eu tenho mais alguns detalhes no meu Blog: http://www.sgrottel.de/?p=1581&lang=en