Compatibilidad de WPF con alto DPI ── causas y soluciones del desenfoque pese a que «debería ser resistente al DPI»

· Actualizado el: · · WPF, Alto DPI, Windows, .NET, C#, .NET Framework, XAML, UI, Consultoría técnica

«Con WPF no hace falta hacer nada para el DPI, ¿verdad?». Recibimos esta pregunta a menudo en las consultas relacionadas con alto DPI. La respuesta es en parte correcta y en parte no. Los síntomas reales que nos traen incluyen: «en el portátil solo se ve perfecto, pero al conectarlo al monitor externo de la sala de reuniones toda la pantalla se difumina», «el texto se ve nítido pero solo los iconos de la barra de herramientas se ven borrosos», «las líneas de separación de una lista se vuelven más gruesas o desaparecen según el lugar» y «solo la parte donde está incrustado un control de formularios antiguo se ve más pequeña». Todos estos casos ocurren de verdad en aplicaciones WPF que «deberían ser resistentes al DPI».

En el artículo del día anterior, «Compatibilidad de WinForms con alto DPI», repasamos el mecanismo de escalado de DPI de Windows, los modos de reconocimiento de DPI (Unaware / System Aware / Per-Monitor V2) y cómo rescatar a las aplicaciones WinForms de la era en que el DPI de 96 se escribía directamente en el código. Este artículo es la versión para WPF. Dejamos la teoría general sobre la virtualización de DPI y los modos de reconocimiento de DPI para el artículo de WinForms, y aquí nos concentramos en lo específico de WPF: por qué y dónde queda un problema en WPF, que se supone compatible con el DPI desde el principio. Los ordenamos en el orden en que se usan en la práctica: cómo diagnosticar los síntomas, cómo declarar la compatibilidad Per-Monitor DPI (para .NET Framework 4.6.2 y para .NET respectivamente), las soluciones contra el difuminado de líneas y mapas de bits, la trampa de mezclar WindowsFormsHost, y por último la tabla de decisión sobre hasta dónde llegar.

Términos usados en este artículo

Dejamos la teoría general sobre los modos de reconocimiento de DPI para el artículo de WinForms, pero aquí repasamos primero los términos que usaremos en el texto sin volver a explicarlos.

Término Significado
DIP Device Independent Pixel (unidad independiente del dispositivo). La unidad de diseño de WPF en la que 1/96 de pulgada equivale a 1. El 120 de Width="120" en XAML es esto
HWND El identificador que Windows asigna a cada ventana (el «window handle»). WPF solo tiene un HWND en la ventana de nivel superior; los botones y el texto interiores los dibuja el propio WPF. Solo los elementos que introducen un HWND quedan «fuera» del dibujado de WPF (capítulo 7)
GDI La API de dibujo 2D tradicional de Windows. Dibuja a nivel de píxel, y la tabla oficial indica que no admite el escalado Per-Monitor DPI1
Colocación en subpíxel Estado en el que el borde de un elemento no cae en una posición entera de píxel físico. Los bordes que quedan en una posición intermedia se dibujan con antialiasing (un proceso que difumina el borde con un color intermedio) y se ven difuminados (apartado 5.1)
Virtualización de DPI Medida de rescate por la que, para aplicaciones Unaware / System Aware, el sistema operativo amplía o reduce el resultado dibujado de la ventana como si fuera un mapa de bits. El diseño no se rompe, pero a cambio todo el conjunto se difumina1
Manifiesto El XML incrustado en el ejecutable. Es la única vía para declarar el modo de reconocimiento de DPI en WPF (capítulo 4)

1. Conclusión general

Antes de nada, estos son los tres puntos que forman la columna vertebral de todo el artículo.

  • WPF sigue correctamente el «DPI del momento de arranque» desde el principio. Como su unidad de diseño es el DIP y el sistema de dibujado escala automáticamente según el DPI del sistema, funciona como System DPI Aware por defecto. A diferencia de WinForms, no hace falta configurar ni revisar nada equivalente a AutoScaleMode.2
  • Aun así, quedan cuatro grupos de problemas: (a) el difuminado general al moverse a un monitor con otro DPI, (b) el desenfoque de imágenes de mapa de bits e iconos, (c) el difuminado de líneas y bordes finos, y (d) el contenido mixto de WindowsFormsHost / WebBrowser y similares. La causa y la solución son distintas en cada caso, así que primero utilice la tabla del capítulo 3 para identificar cuál es.
  • El trabajo del lado de WPF se reduce a «una línea de declaración + una revisión del resto». Como el framework se encarga de que el diseño siga al DPI, lo único que realmente hay que corregir es el código que maneja píxeles directamente, los recursos de mapa de bits y el contenido mixto (capítulo 8).

A continuación, la conclusión de cada uno de los cuatro grupos.

Grupo Conclusión Detalle
(a) Difuminado general La solución definitiva es la compatibilidad Per-Monitor DPI. WPF la admite como framework desde .NET Framework 4.6.2, y basta con declararla en el manifiesto para que el reescalado de la ventana ocurra automáticamente34. El WPF de .NET (Core 3.1 a .NET 8) también sigue siendo System Aware por defecto, y WPF no dispone de un mecanismo equivalente a ApplicationHighDpiMode / SetHighDpiMode de WinForms Capítulo 4
(b) Desenfoque de mapas de bits La primera opción es usar recursos vectoriales como Path / Geometry. Si solo existe el mapa de bits, se preparan varias resoluciones y se alternan según el DPI. La interpolación predeterminada de WPF (Linear) es la que más desenfoca las imágenes pequeñas, como los iconos, en escalas no enteras5 Apartado 5.3
(c) Difuminado de líneas finas Un problema de colocación en subpíxel anterior al propio DPI. El primer remedio es UseLayoutRounding="True" en el elemento raíz; SnapsToDevicePixels es una herramienta distinta que ajusta en el momento del dibujado. Ambas están desactivadas por defecto67 Apartado 5.1
(d) Contenido mixto El elemento que determina el límite de la compatibilidad Per-Monitor. En particular, se indica explícitamente que el comportamiento Per-Monitor de WPF cuando está «alojado» en ElementHost / HwndSource no cuenta con soporte oficial4 Capítulo 7

Por lo demás, la teoría general sobre los modos de reconocimiento de DPI (la virtualización de DPI, la diferencia entre System Aware y Per-Monitor V2, la posibilidad de que el usuario la sobrescriba desde las propiedades del ejecutable) y cómo montar un entorno de pruebas con DPI mixto se comparten con los capítulos 2 a 3 y 7 del artículo de WinForms, así que no los repetimos aquí.

2. Por qué WPF es «resistente al DPI» ── DIP y System DPI Aware

La raíz del problema de alto DPI en WinForms está en que tanto las coordenadas como los tamaños se escriben directamente en píxeles físicos, y el sistema añadió a posteriori un mecanismo (AutoScale) que recalcula en tiempo de ejecución un diseño pensado para 96 DPI. WPF parte de un punto distinto. El 120 de Width="120" en XAML no son píxeles físicos, sino la unidad independiente del dispositivo (DIP), en la que 1/96 de pulgada equivale a 1. Todo el diseño se calcula en esta unidad y, en el momento de dibujar, se convierte según el factor correspondiente al DPI del sistema (1.5 veces para el 150 %). El texto también se dibuja como fuente vectorial, por lo que no sufre la degradación típica de los mapas de bits al ampliarse. Gracias a este mecanismo, las aplicaciones WPF funcionan como System DPI Aware sin necesidad de declarar nada.2

Aquí tiene un resumen de las diferencias con WinForms.

  WinForms WPF
Unidad de coordenadas y tamaño Píxeles físicos DIP (1/96 de pulgada)
Sin declarar nada Unaware (se difumina por la ampliación de mapa de bits del SO) System Aware (nítido en el monitor principal)
Seguimiento del DPI al arrancar Recálculo por formulario mediante AutoScaleMode. Requiere configuración y revisión El sistema de dibujado escala automáticamente. No requiere configuración
Rotura típica con alto DPI Solapamiento y recorte de un diseño de coordenadas fijas El diseño no se rompe; aparece como difuminado o desenfoque
Accidentes causados por el diseñador Reescritura de AutoScaleDimensions (algo habitual) En principio, ninguno (el XAML se guarda tal cual en DIP)

Como muestra la tabla, el trabajo que en WinForms ocupaba la mayor parte del esfuerzo —«corregir la rotura del diseño» y «evitar accidentes del diseñador»— apenas existe en WPF. La idea de que «WPF es resistente al DPI» es correcta.

No obstante, lo automático llega solo hasta el alcance de System Aware. System Aware es el modo que «se ajusta al DPI del monitor principal en el momento de iniciar sesión», y no incluye el seguimiento (Per-Monitor) en entornos donde cada monitor tiene un DPI distinto. Además, lo que se amplía en DIP es únicamente lo que WPF dibuja por sí mismo (texto, formas, controles): las imágenes de mapa de bits se desenfocan al ampliarse, y el contenido mixto que introduce un HWND queda fuera del escalado de WPF. Las consultas del tipo «se supone que es resistente al DPI, pero…» surgen casi siempre en esta parte restante.

3. Problemas que aún ocurren ── identificar la causa a partir de los síntomas

Esta es la tabla de diagnóstico que utilizamos en primer lugar cuando recibimos una consulta. En WPF, la correspondencia entre síntoma y causa es aún más clara que en WinForms, así que con esta tabla suele bastar para identificar la causa.

Síntoma Causa Solución
Al mover la ventana a un monitor con otro DPI, toda la ventana se difumina de manera uniforme. Vuelve a verse bien al regresar al monitor original Sigue en System Aware. El sistema operativo amplía toda la ventana como si fuera un mapa de bits Capítulo 4 (adoptar Per-Monitor)
Se difumina justo después de cambiar la configuración de escalado, o al conectarse por RDP desde un cliente de alto DPI Lo mismo (el DPI del sistema queda fijado en el momento de iniciar sesión) Capítulo 4
El texto se ve nítido pero solo los iconos o las imágenes se ven borrosos o pequeños Los recursos de mapa de bits se están ampliando por interpolación Apartado 5.3
Una línea o un borde de 1 px tiene un grosor distinto según el lugar, o se ve ligeramente difuminado Colocación en subpíxel y antialiasing Apartado 5.1
El texto pequeño se ve difuminado en un monitor al 100 % (96 DPI) Antialiasing del modo de formato de texto predeterminado (Ideal) Apartado 5.2
Las imágenes creadas manualmente (WriteableBitmap, RenderTargetBitmap, etc.) se ven borrosas El tamaño en píxeles se calcula asumiendo 96 DPI Capítulo 6
La posición de la ventana, las coordenadas del ratón o las de una captura de pantalla no coinciden Confusión entre DIP y píxeles físicos Capítulo 6
Solo el interior de WindowsFormsHost / WebBrowser / algunos controles de terceros se ve pequeño, tosco o roto Contenido mixto (fuera del escalado de WPF) Capítulo 7

Una aclaración sobre la primera fila, «toda la ventana se difumina de manera uniforme». Se trata del estado en que la virtualización de DPI (la ampliación de mapa de bits del sistema operativo), descrita en el capítulo 2 del artículo de WinForms, actúa sobre una aplicación System Aware; la aplicación no está rota. Una aplicación System Aware dibuja según «el DPI del monitor principal en el momento de iniciar sesión», y en cualquier otro monitor con un DPI distinto el sistema operativo amplía o reduce la imagen para compensar. Es una medida de rescate del sistema operativo por la que el diseño no se rompe, pero a cambio se difumina.2 Solucionarlo significa rechazar este rescate y declarar «yo mismo seguiré el DPI de cada monitor», es decir, adoptar Per-Monitor.

Por el contrario, los síntomas de la tercera fila en adelante se pueden corregir sin dejar System Aware. Si la consulta es «no usamos varios monitores, pero al 150 % los iconos y las líneas se ven mal», puede saltarse el capítulo 4 y empezar directamente por el capítulo 5.

3.1 Procedimiento para reproducir y comprobar los síntomas en su propio equipo

Este artículo no incluye capturas de pantalla. El difuminado y el desenfoque se aprecian con más certeza reproduciéndolos en un equipo real que mostrándolos en una imagen estática. Siga los pasos siguientes para comprobar con sus propios ojos los síntomas de la tabla anterior. Cómo montar el propio entorno de pruebas con DPI mixto se explica en el capítulo 7 del artículo de WinForms, pero incluso con un solo monitor se puede llegar hasta aquí.

  1. Cambie el porcentaje de escalado. Vaya a Inicio > Configuración > Sistema > Pantalla > «Escala y diseño» > «Cambiar el tamaño de texto, apps y otros elementos» para alternar entre 100 %, 125 % y 150 %.8 El efecto se nota más al 125 % y al 150 % (porque son múltiplos no enteros, como se explica en el apartado 4.1).
  2. Reinicie la aplicación. Como WPF es System Aware por defecto, una aplicación que ya está en ejecución no sigue el nuevo porcentaje de escalado. Después de cambiarlo, reinícela sin falta. Si aun así no se corrige, cierre sesión y vuelva a iniciarla (porque el DPI del sistema queda fijado en el momento de iniciar sesión).
  3. Observe los detalles con la lupa de Windows. Con la tecla de logotipo de Windows + + (más) se abre la lupa.9 Suba el zoom hasta aproximadamente el 400 % y observe estos tres puntos en orden:
    • Líneas y bordes ── si un gris tenue sobresale 1 px en uno de los lados de la línea, o en ambos, es la colocación en subpíxel del apartado 5.1. La señal definitiva es que, con la misma línea especificada como 1, la intensidad o el grosor se ven distintos según la fila.
    • Iconos e imágenes ── si el contorno se difumina por duplicado y una línea que debería medir 1 px queda como un color intermedio de 2 px, es la ampliación por interpolación de mapas de bits del apartado 5.3. Que el texto de la misma pantalla se vea nítido es el criterio decisivo para diferenciarlo (el texto es vectorial y no se desenfoca).
    • Toda la pantalla ── si el texto, las líneas y los iconos se ven igual de borrosos, no es un problema de dibujado individual, sino la virtualización de DPI del sistema operativo. En ese caso, pase al capítulo 4.
  4. Mueva la ventana entre monitores (si dispone de dos o más). Arrastre la ventana a un monitor con un porcentaje de escalado distinto. Si se difumina al llegar y se corrige al volver, corresponde a la primera fila de la tabla. Por el contrario, si el aspecto no cambia al moverla, el problema (a) no está ocurriendo.
  5. Cambie el porcentaje de escalado mientras la aplicación está en ejecución. Modifique la configuración del paso 1 sin cerrar la aplicación. Una aplicación System Aware se difumina de inmediato.

Estos pasos del 1 al 5 también sirven tal cual como procedimiento de revisión después de adoptar Per-Monitor. Los puntos de prueba recomendados oficialmente también son cuatro: mover la ventana entre monitores, iniciarla con distintos DPI, cambiar el porcentaje de escalado en tiempo de ejecución, y cerrar e iniciar sesión de nuevo tras cambiar el monitor principal.1

4. El difuminado con varios monitores ── compatibilidad Per-Monitor DPI

4.1 Los límites de System Aware

El DPI del sistema queda fijado en el momento de iniciar sesión, según el DPI del monitor principal. Si la pantalla integrada del portátil al 150 % es el monitor principal, WPF dibuja todas las ventanas a 1.5 veces, y al moverlas a un monitor externo al 100 % el sistema operativo las muestra reducidas a 2/3. Este escalado del sistema operativo se ve especialmente borroso cuando el factor no es un número entero.2 Se puede comprobar empíricamente que una combinación intermedia como 125 % y 150 % produce una degradación visual mayor que la diferencia del doble entre 200 % y 100 %.

En definitiva, los problemas de un WPF System Aware se limitan a dos situaciones: usarlo desplazándose entre monitores con DPI distinto, y que la configuración de escalado cambie después de iniciar sesión (por ejemplo, que el monitor principal cambie al conectar o desconectar una estación de acoplamiento, o que se introduzca el DPI del lado cliente a través de RDP). En una aplicación interna que todos usan en un escritorio fijo con un solo monitor, quedarse en System Aware resulta suficiente en la práctica. Retomaremos esta decisión en la tabla del capítulo 8.

4.2 Cómo declararlo en .NET Framework 4.6.2 y posteriores

La compatibilidad Per-Monitor DPI de WPF se introdujo en .NET Framework 4.6.2.3 Antes de eso, aunque se declarara Per-Monitor ante el sistema operativo, el lado de WPF no seguía los cambios de DPI, y había que escribir todo el reescalado de la ventana por cuenta propia (por eso los ejemplos de la época de Windows 8.1 tenían una estructura tan elaborada, con una DLL auxiliar nativa). Desde 4.6.2, WPF procesa WM_DPICHANGED y se encarga automáticamente del ajuste del tamaño de la ventana, el rediseño y el redibujado.

Los requisitos son dos: un sistema operativo Windows 10 Anniversary Update (1607) o posterior, y una compilación con destino .NET Framework 4.6.2 o posterior.4 La declaración se escribe en el manifiesto de la aplicación.

<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:windowsSettings>
    <!-- En .NET Framework 4.6.2 a 4.7.2, declarar solo PerMonitor (igual que la guía para desarrolladores).
         La compatibilidad de WPF con PerMonitorV2 llega con .NET Framework 4.8, así que
         en 4.8+ / .NET se escribe "PerMonitorV2, PerMonitor" con V2 al principio -->
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"
      >PerMonitor</dpiAwareness>
    <!-- Para sistemas operativos antiguos que no reconocen dpiAwareness (recurre a System Aware) -->
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  </asmv3:windowsSettings>
</asmv3:application>

Esta estructura de dos niveles tiene sentido. El elemento dpiAwareness se reconoce desde Windows 10 1607, y de los valores separados por comas se utiliza el primero que el sistema pueda reconocer.10 En un sistema operativo antiguo que ni siquiera conoce el elemento dpiAwareness, se recurre a la declaración de dpiAware (System Aware); ese es el mecanismo.

Preste atención aquí a usar PerMonitor o PerMonitorV2 según el framework de destino. WPF admite oficialmente PerMonitorV2 (y Mixed-Mode DPI) solo a partir de .NET Framework 4.811, y el ejemplo de la guía para desarrolladores de 4.6.2 también declara únicamente PerMonitor.4 Si escribe V2 en primer lugar con un destino de 4.6.2 a 4.7.2, un sistema operativo Windows 10 1703 o posterior seleccionará el modo V2, que el framework no admite, y se producirán comportamientos inesperados, en particular en torno al contenido mixto como WindowsFormsHost. Nuestra recomendación es subir primero a 4.8 o posterior (o al WPF de .NET) y entonces escribir PerMonitorV2, PerMonitor con V2 al principio, dejando en manos del sistema operativo el escalado del área no cliente, como la barra de título y las barras de desplazamiento (véase el capítulo 3 del artículo de WinForms).

Hay una trampa relacionada con el framework de destino. Aunque el .NET Framework instalado en el entorno de ejecución sea 4.6.2 o posterior, si el destino del proyecto sigue siendo 4.6.1 o anterior, el seguimiento Per-Monitor está desactivado por defecto. En ese caso, se activa explícitamente mediante un AppContext switch en app.config.4

<configuration>
  <runtime>
    <!-- Cuidado con la doble negación: poner en false "no escalar ante cambios de DPI" equivale a activarlo -->
    <AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
  </runtime>
</configuration>

Por experiencia, cuando nos consultan «he escrito el manifiesto pero no funciona», la causa suele ser casi siempre esta cuestión del framework de destino, o bien que el manifiesto no se está incluyendo en la compilación (la configuración del proyecto sigue apuntando al manifiesto predeterminado).

4.3 Cómo declararlo en .NET (Core 3.1 a .NET 8)

El WPF sobre .NET también sigue siendo System Aware por defecto sin un manifiesto. Frente a WinForms, que dispone de una vía específica mediante ApplicationHighDpiMode en el archivo de proyecto o Application.SetHighDpiMode, WPF no cuenta con un mecanismo oficial para cambiar el modo de reconocimiento de DPI desde el código o la configuración del proyecto. El método de declaración es el mismo que en .NET Framework.

El procedimiento para añadir el manifiesto también se puede describir sin capturas de pantalla, así que enumeramos directamente los nombres de los menús.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y elija «Agregar» > «Nuevo elemento» (el atajo es Ctrl+Shift+A).
  2. En la lista, seleccione la plantilla «Archivo de manifiesto de aplicación» y agréguela con el nombre predeterminado, app.manifest.
  3. Abra el app.manifest agregado y escriba la misma declaración dpiAwareness del apartado 4.2 como elemento <asmv3:application>. La plantilla ya incluye de partida elementos como requestedExecutionLevel (el nivel de ejecución solicitado por UAC); no los elimine, añada la declaración sin borrarlos.
  4. Escriba <ApplicationManifest>app.manifest</ApplicationManifest> dentro del <PropertyGroup> del archivo de proyecto para hacer referencia a él (en los proyectos de estilo SDK, a veces se reconoce automáticamente con solo agregarlo con el nombre app.manifest, pero es más seguro declararlo explícitamente).

En un proyecto de .NET Framework, en lugar del paso 4, seleccione el manifiesto agregado en el menú desplegable de Propiedades del proyecto > pestaña «Aplicación» > «Manifiesto».

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <ApplicationManifest>app.manifest</ApplicationManifest>
  </PropertyGroup>
</Project>

Como el runtime ya es reciente, no hace falta el AppContext switch del apartado 4.2. Tenga en cuenta únicamente que migrar a .NET no resuelve automáticamente el alto DPI. La migración simplifica las cosas del lado de WinForms (apartado 4.1 del artículo de WinForms); WPF ya había alcanzado el mismo nivel actual en la época de .NET Framework 4.6.2.

Que quede trabajo por hacer después de la declaración es igual que en WinForms, pero el contenido es distinto. En WinForms, el terreno principal era corregir la rotura del diseño. En WPF, como el framework se encarga de que el diseño siga al DPI, lo que queda es la revisión visual de todas las pantallas, el cambio de recursos de mapa de bits según el DPI (apartado 5.3), el seguimiento en el código que maneja píxeles directamente (capítulo 6) y la comprobación del contenido mixto (capítulo 7). Con el mismo número de pantallas, el esfuerzo total de adoptar Per-Monitor suele ser un escalón menor que en WinForms.

5. Difuminado y desenfoque en el dibujado ── líneas finas, texto y mapas de bits

El contenido de este capítulo es independiente de adoptar Per-Monitor. Funciona igual si se queda en System Aware, así que merece la pena aplicarlo incluso en aplicaciones sin uso de varios monitores.

5.1 El difuminado de líneas y bordes finos ── UseLayoutRounding y SnapsToDevicePixels

Diseñar en DIP significa que el borde de un elemento no siempre cae en una posición entera de píxel físico. Al 125 %, 1 DIP equivale a 1.25 px, así que una línea de 1 DIP de ancho corresponde a 1.25 píxeles físicos, y el borde que cae a mitad de píxel se dibuja semitransparente por el antialiasing. El resultado es que «la línea se difumina» o que «con la misma línea especificada como 1 px, el grosor se ve distinto según la fila». Incluso a 96 DPI ocurre lo mismo si la división de un tamaño con asterisco (*) en un Grid, o el cálculo del margen de un centrado, caen en 0.5 px. Esto forma parte de las especificaciones del dibujado en subpíxel de WPF; más que un problema de DPI, es un problema que se hace más visible cuanto mayor es el DPI.6

El primer remedio es el redondeo de diseño. Basta con añadir una línea al elemento raíz.

<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        UseLayoutRounding="True">

UseLayoutRounding es un mecanismo que redondea los valores de píxel no enteros durante el paso de diseño; está desactivado por defecto, y al establecerlo en la raíz se propaga a todo el árbol visual.6 SnapsToDevicePixels, de nombre parecido, cumple un papel distinto: no actúa en el diseño, sino que ajusta los bordes a los límites de píxel en el momento del dibujado. También está en false por defecto, y el ajuste se hereda al subárbol. La documentación oficial menciona su uso para reducir el difuminado por antialiasing alrededor de líneas finas en entornos con más de 96 DPI.7

  UseLayoutRounding SnapsToDevicePixels
Cuándo actúa En el paso de diseño (redondea a píxeles enteros el resultado de Measure / Arrange) En el momento de dibujar (ajusta los bordes a los límites de píxel)
Valor por defecto false false
Alcance Al establecerlo en la raíz, se propaga a los descendientes También se hereda a los descendientes
Dónde surte más efecto En el diseño en general. Desviaciones de 0.5 px en el tamaño o la posición de los elementos, y el difuminado de líneas y bordes que de ahí se deriva El difuminado residual de elementos individuales, como Border o líneas finas
Pauta de uso Aplicar primero esto en la Window raíz. En una aplicación nueva, en el estilo común de todas las ventanas De forma puntual en los puntos que aún queden después de UseLayoutRounding

Si tiene dudas, siga este orden: «UseLayoutRounding="True" en la raíz y, en los puntos que aún queden, SnapsToDevicePixels="True"». Como efecto secundario del redondeo, el ancho de columnas repartidas a partes iguales con un tamaño de asterisco puede volverse desigual en incrementos de 1 px, pero en la práctica casi nunca hemos visto que esto se convierta en un problema en las pantallas de aplicaciones de negocio. Para el difuminado en código que dibuja por su cuenta con DrawingContext, existe también el recurso de bajo nivel GuidelineSet, pero en la mayoría de los casos basta con redondear las coordenadas de dibujado.

5.2 El difuminado del texto ── TextFormattingMode

La antigua queja de que «en WPF el texto se ve tenue o difuminado» es más una cuestión del modo de formato de texto que del DPI. El formato de texto de WPF tiene dos modos: Ideal (predeterminado), que coloca el texto según las métricas ideales propias de la fuente, y Display, que lo coloca con métricas compatibles con GDI.12 Ideal produce un espaciado entre caracteres más elegante y se comporta mejor con el escalado, pero al dibujar texto pequeño a 96 DPI (100 %) puede verse difuminado por el antialiasing. Al establecer TextOptions.TextFormattingMode="Display" en la raíz se obtiene una nitidez al estilo de WinForms.

Sin embargo, en un entorno de alto DPI la situación se invierte. Como Display ajusta el texto a la cuadrícula de píxeles de 96 DPI, bajo escalado la calidad puede incluso empeorar. La decisión práctica habitual es: Display si la mayoría de los usuarios trabaja al 100 %, e Ideal (el valor predeterminado) si el entorno estándar actual tiene el 125 % o más como norma. También es posible implementar un cambio a Display solo cuando se está al 100 %, pero las pantallas donde vale la pena llegar a ese nivel (pantallas de tipo editor de texto) son limitadas.

5.3 El desenfoque de imágenes e iconos ── prioridad a lo vectorial y varias resoluciones

WPF también coloca en DIP el mapa de bits especificado en Image y lo amplía según el DPI. Un icono de 16×16 px se amplía por interpolación a 24×24 px en un entorno al 150 %, y con el algoritmo de interpolación predeterminado (Linear) se desenfoca con claridad.5 El aspecto de «el texto se ve bien pero solo los iconos se ven apagados» en las aplicaciones WPF se debe casi siempre a esto.

Hay tres soluciones, en orden de prioridad.

  1. Convertirlo en un recurso vectorial (primera opción). Un icono guardado como Path, Geometry o DrawingImage se dibuja con nitidez en cualquier DPI y no necesita código para alternar entre versiones. Existen abundantes herramientas de diseño y conversores de SVG a XAML. Merece la pena establecer como norma que los iconos de una aplicación nueva se guarden como vectores desde el principio. Las fuentes de iconos, como Segoe MDL2 Assets, producen el mismo efecto.
  2. Alternar entre mapas de bits de varias resoluciones según el DPI. Para recursos que no se pueden convertir en vectores, como fotografías o capturas de pantalla, se preparan versiones para 96 / 120 / 144 / 192 DPI (por ejemplo, 16 / 20 / 24 / 32 px) y se elige la adecuada según el DPI actual. La guía oficial para desarrolladores también recomienda alternar recursos según el DPI como medida contra el desenfoque.2
  3. Elegir la interpolación con RenderOptions.BitmapScalingMode. Es una medida paliativa cuando no se pueden añadir más recursos. Con un múltiplo entero como el 200 %, ampliar manteniendo los puntos con NearestNeighbor da un resultado más nítido, y para reducir imágenes grandes conviene HighQuality (Fant).5

El cambio del punto 2, si ya se ha adoptado Per-Monitor, se realiza en OnDpiChanged (disponible desde .NET Framework 4.6.2).

public partial class MainWindow : Window
{
    protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
    {
        base.OnDpiChanged(oldDpi, newDpi);
        // Se llama cada vez que se mueve entre monitores o cambia el escalado
        AppIcon.Source = IconAssets.SelectFor(newDpi.DpiScaleX);
    }
}

public static class IconAssets
{
    // Elige el recurso adecuado entre los preparados a 16 / 24 / 32 px según el factor de escala
    public static BitmapImage SelectFor(double scale) => scale switch
    {
        <= 1.0 => Load("icon16.png"),
        <= 1.5 => Load("icon24.png"),
        _      => Load("icon32.png"),
    };

    private static BitmapImage Load(string name) =>
        new(new Uri($"pack://application:,,,/Assets/{name}"));
}

Si se queda en System Aware, el DPI no cambia después de arrancar, así que basta con elegirlo una sola vez al iniciar (Loaded) con VisualTreeHelper.GetDpi(this). Antes de escribir el código de alternancia, preguntarse cada vez «¿no se podría convertir este recurso en vectorial?» acaba siendo, con diferencia, lo más fácil de mantener.

6. El código que maneja píxeles y el seguimiento de los cambios de DPI

Lo que queda fuera del paraguas del escalado automático de WPF es el código que cuenta píxeles físicos por su cuenta. Los casos típicos son los tres siguientes, y todos tienden a manifestarse de la peor manera: «perfecto en la máquina de desarrollo (100 %), pero solo se ve borroso o desplazado en el 150 % del cliente».

El primero es el código que crea su propio búfer de píxeles, como WriteableBitmap o RenderTargetBitmap. Si se crea con un número de píxeles igual al tamaño en DIP, en un entorno al 150 % se amplía por interpolación 1.5 veces y se difumina. El DPI actual se obtiene con la estructura DpiScale que devuelve VisualTreeHelper.GetDpi, así que hay que expresar el número de píxeles en píxeles físicos y grabar correctamente el DPI en el búfer.

// imageHost: elemento que muestra el resultado dibujado (ActualWidth/Height están en DIP)
DpiScale dpi = VisualTreeHelper.GetDpi(imageHost);
int pixelWidth  = (int)Math.Ceiling(imageHost.ActualWidth  * dpi.DpiScaleX);
int pixelHeight = (int)Math.Ceiling(imageHost.ActualHeight * dpi.DpiScaleY);

// No lo cree fijo en 96,96: pase el DPI real (así 1 píxel físico = 1 píxel del búfer)
var bitmap = new WriteableBitmap(
    pixelWidth, pixelHeight,
    dpi.PixelsPerInchX, dpi.PixelsPerInchY,
    PixelFormats.Bgra32, null);

El segundo son las coordenadas de pantalla y la interoperabilidad con Win32. Las coordenadas que devuelve PointToScreen, las que se reciben en WM_MOUSEMOVE o en un hook, y las que se pasan a MoveWindow están todas en píxeles físicos; si se mezclan con el DIP del lado XAML, se desplazan 1.25 veces en un entorno al 125 %. Para convertirlas se usa la matriz de CompositionTarget.

var source = PresentationSource.FromVisual(this);
if (source?.CompositionTarget is { } target)
{
    // DIP → píxeles físicos
    Point device = target.TransformToDevice.Transform(new Point(x, y));
    // Píxeles físicos → DIP
    Point dip = target.TransformFromDevice.Transform(devicePoint);
}

El tercero es la caché. Si se almacenan en caché mapas de bits, valores de diseño o texto formateado que se generaron incorporando el DPI, al adoptar Per-Monitor quedan obsoletos cada vez que se mueve la ventana entre monitores. Igual que en el apartado 5.3, regenérelos en OnDpiChanged (o en el evento DpiChanged de la ventana). Aquí también, si se queda en System Aware, el DPI queda fijado al arrancar y no hace falta código de seguimiento. Estimar que «el coste de adoptar Per-Monitor equivale al número de fragmentos de código que manejan píxeles» encaja bien con la realidad.

Por cierto, la forma de tratar el hilo de la interfaz cuando este tipo de regeneración se realiza de forma asíncrona está explicada en «async y el hilo de la interfaz en WPF/WinForms».

7. La trampa del contenido mixto ── WindowsFormsHost, WebBrowser y terceros

El escalado automático de WPF solo alcanza a lo que el propio WPF dibuja. Los elementos que introducen un HWND quedan «fuera» del dibujado de WPF, por lo que el tratamiento del DPI se complica un nivel más. La documentación oficial de Windows indica expresamente que «otro framework alojado en WPF, o WPF alojado en otro framework, no se escala automáticamente».1

Forma de mezcla Qué ocurre Tratamiento realista
WindowsFormsHost (alojar WinForms dentro de WPF) El host convierte entre los dos sistemas de coordenadas, DIP y píxeles físicos, pero el escalado del contenido interior llega solo hasta donde el propio control de WinForms lo admita13 Revisar la compatibilidad DPI del lado WinForms interior (AutoScaleMode, etc.) con los criterios del artículo de WinForms. No esperar seguimiento Per-Monitor
Control WebBrowser (motor de IE) Contenido nativo con un HWND independiente. El porcentaje de zoom puede no coincidir con el escalado de la aplicación Tratarlo como legado. Añadirlo como argumento al considerar la migración a WebView2
ElementHost / HwndSource (alojar WPF dentro de WinForms o Win32) El escenario Per-Monitor no cuenta con soporte oficial4 Diseñar como máximo hasta System Aware, ajustándose al modo de reconocimiento de DPI de la aplicación anfitriona
Controles de terceros El nivel de soporte varía según el producto. Un producto sin compatibilidad Per-Monitor se convierte en el límite de esa pantalla Inventariar el estado y las versiones compatibles de cada proveedor antes de decidir si adoptar Per-Monitor

En la práctica, el caso más frecuente es el de la primera fila. Cuando una aplicación con la estructura «migramos a WPF, pero la vista previa de informes o los gráficos siguen usando un control antiguo de WinForms / ActiveX alojado con WindowsFormsHost» adopta Per-Monitor, la parte WPF sigue el DPI a la perfección, pero solo el interior del host no sigue el DPI tras el movimiento y se queda pequeño y tosco. A nivel de Win32 existe también un mecanismo para alojar DPI mixto (SetThreadDpiHostingBehavior), pero no es algo que se pueda usar cómodamente desde WPF. En nuestro caso, asumimos que «la parte mixta determina el límite de la compatibilidad Per-Monitor» y empezamos por elaborar un inventario del contenido mixto. Los criterios para orientarse hacia eliminar la mezcla están recogidos en «Cómo elegir entre WinForms, WPF y WinUI» y en «Mantener, envolver o reemplazar ActiveX/OCX».

8. Hasta dónde llegar ── tabla de decisión y avance por etapas

En el caso de WPF, las opciones se reducen en la práctica a dos (no existe el equivalente a «no hacer nada = Unaware» de WinForms, porque sin declarar nada ya se es System Aware).

  Quedarse en System Aware Adoptar Per-Monitor
Aspecto Nítido en el monitor principal. Se difumina en monitores con otro DPI, tras cambiar el escalado o por RDP Nítido en todos los monitores
Trabajo de declaración No requerido (es el valor predeterminado) Solo añadir el manifiesto (capítulo 4)
Trabajo restante tras declarar ── Revisión visual de todas las pantallas + cambio de recursos de mapa de bits según el DPI (apartado 5.3) + soporte de DpiChanged en el código que maneja píxeles (capítulo 6) + comprobación del contenido mixto (capítulo 7)
Elemento que determina el límite ── WindowsFormsHost, WebBrowser, controles de terceros
Casos adecuados Aplicaciones internas centradas en un escritorio fijo de un solo monitor. Aplicaciones con mucho contenido mixto Uso mixto de portátil y monitor externo, aplicaciones principales de vida larga, productos que se distribuyen a clientes

Los criterios de decisión son los mismos que en el capítulo 7 del artículo de WinForms (la vida útil de la aplicación, el entorno de uso, el presupuesto de modificación), pero la diferencia es que en WPF el coste marginal de adoptar Per-Monitor es pequeño. La declaración es un solo archivo, el framework se encarga de que el diseño siga al DPI, y el trabajo restante es proporcional a la cantidad de código relacionado con píxeles y de recursos. En una aplicación con poco contenido mixto, la relación coste-beneficio de llegar hasta Per-Monitor es claramente más alta que en WinForms. El avance en tres etapas que recomendamos en nuestros proyectos reales es el siguiente.

  1. Mejora de calidad quedándose en System Aware: UseLayoutRounding en la raíz, conversión de iconos a vectores o a varias resoluciones, corrección del DPI en la familia WriteableBitmap. Todo lo que no sea el difuminado con varios monitores se resuelve aquí, y aunque se decida no adoptar Per-Monitor, este trabajo queda como un activo aprovechable.
  2. Declaración y revisión de Per-Monitor: añadir el manifiesto y recorrer todas las pantallas en un entorno de DPI mixto para identificar los puntos problemáticos. Los problemas que se encuentren aquí deberían poder clasificarse en alguno de los capítulos 5 a 7.
  3. Implementación del seguimiento de los cambios de DPI: cambio de recursos y regeneración de caché en OnDpiChanged. En esta etapa se decide, para el contenido mixto, «hasta dónde se acepta como está».

El entorno de pruebas puede ser el mismo que describimos en el capítulo 7 del artículo de WinForms (dos monitores con escalados distintos, intercambio del monitor principal más reinicio de sesión, cambio de escalado en tiempo de ejecución, RDP desde un cliente de alto DPI).1 En WPF conviene poner el foco en las líneas, los iconos y el dibujado propio con múltiplos no enteros (125 % / 150 %), y en el comportamiento al arrastrar la ventana entre monitores. Conocer de antemano la distribución del entorno de uso (qué porcentaje de usuarios trabaja con varios monitores) evita que la decisión de inversión se tambalee. Este punto de vista también lo tratamos en «Diseño UX de aplicaciones Windows».

9. Resumen

Gracias al DIP y al escalado automático, WPF es System DPI Aware desde el principio, y la «lucha contra la rotura del diseño» que era el terreno principal de WinForms prácticamente no existe. Aun así, quedan cuatro grupos de problemas —el difuminado con varios monitores (falta de compatibilidad Per-Monitor), el desenfoque de mapas de bits, el difuminado de líneas finas y el contenido mixto—, y para cada uno existe una solución establecida.

  • El difuminado con varios monitores se resuelve con solo declarar el manifiesto, ya que a partir de .NET Framework 4.6.2 y en el WPF de .NET el seguimiento Per-Monitor se obtiene automáticamente
  • Para las líneas y bordes finos, el primer remedio es UseLayoutRounding="True" en la raíz, y SnapsToDevicePixels para lo que quede
  • Para los iconos, la primera opción son los recursos vectoriales; si son mapas de bits, varias resoluciones más cambio según el DPI
  • El único trabajo de modificación real es el código que maneja píxeles, como WriteableBitmap o las coordenadas de pantalla; su cantidad determina el esfuerzo de adoptar Per-Monitor
  • WindowsFormsHost, WebBrowser y los componentes de terceros determinan el límite de la compatibilidad. Hágase primero un inventario

Incluso las aplicaciones que se habían quedado en el «con WPF debería bastar», al mirarlas con esta clasificación, suelen tener solo un puñado de puntos que corregir. Si tiene dudas sobre hasta dónde se puede corregir su aplicación, o necesita ayuda con un estudio de la situación actual que incluya el contenido mixto y con la decisión del nivel de compatibilidad a alcanzar, podemos ayudarle.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC atiende consultas sobre la compatibilidad de aplicaciones WPF / WinForms con alto DPI (estudio de la situación actual, decisión sobre la viabilidad de adoptar Per-Monitor, inventario y corrección del contenido mixto), la investigación de fallos de visualización derivados de la renovación de equipos o la introducción de monitores 4K, y la renovación de la interfaz de usuario.

Referencias

  1. Microsoft Learn, High DPI Desktop Application Development on Windows. Sobre la tabla de compatibilidad Per-Monitor por framework de interfaz, que otro framework alojado en WPF u otro framework que aloja WPF no se escalan automáticamente, y los aspectos a probar en un entorno de DPI mixto.  2 3 4 5

  2. Microsoft Learn, Developing a Per-Monitor DPI-Aware WPF Application. Sobre que WPF es System DPI Aware por defecto, el mecanismo de escalado automático mediante DIP, que al moverse a un monitor con otro DPI el sistema operativo escala y se desenfoca especialmente con factores no enteros, y el criterio para alternar recursos de mapa de bits según el DPI.  2 3 4 5

  3. Microsoft Learn, What’s new in .NET Framework. Sobre la activación de Per-Monitor DPI awareness en el WPF de .NET Framework 4.6.2, el switch Switch.System.Windows.DoNotScaleForDpiChanges y la guía para desarrolladores en GitHub.  2

  4. GitHub (microsoft/WPF-Samples), Per Monitor DPI Developer Guide. Sobre los requisitos de Windows 10 Anniversary Update y .NET Framework 4.6.2 o posterior, la escritura de dpiAwareness / dpiAware en el manifiesto, el AppContextSwitchOverrides para destinos anteriores a 4.6.2, y que la configuración con WPF alojado en HwndSource / ElementHost no cuenta con soporte Per-Monitor.  2 3 4 5 6

  5. Microsoft Learn, BitmapScalingMode Enum. Sobre que el valor predeterminado (Unspecified) equivale a Linear, y las características de los algoritmos de interpolación HighQuality (Fant) y NearestNeighbor.  2 3

  6. Microsoft Learn, Layout - WPF. Sobre el diseño en DIP y el mecanismo por el que el dibujado en subpíxel desenfoca los bordes, que el redondeo de diseño (UseLayoutRounding) está desactivado por defecto, y que al establecerlo en el elemento raíz se propaga al árbol visual.  2 3

  7. Microsoft Learn, UIElement.SnapsToDevicePixels Property. Sobre que el valor predeterminado es false, que al establecerlo en la raíz se hereda al subárbol, y que puede reducir los artefactos visuales por antialiasing alrededor de líneas finas en entornos con más de 96 DPI.  2

  8. Soporte de Microsoft, Cambiar la resolución de pantalla y el diseño en Windows. Sobre el procedimiento para elegir el porcentaje de escalado desde «Inicio > Configuración > Sistema > Pantalla», en «Escala y diseño» > «Cambiar el tamaño de texto, apps y otros elementos». 

  9. Soporte de Microsoft, Usar la lupa para ver mejor los elementos de la pantalla. Sobre que con la tecla de logotipo de Windows + el signo más se abre la lupa, con la que se puede mostrar ampliada una parte de la pantalla. 

  10. Microsoft Learn, Setting the default DPI awareness for a process. Sobre que el elemento dpiAwareness (desde Windows 10 1607) tiene prioridad sobre dpiAware, y el comportamiento de reserva por el que, de los valores enumerados separados por comas, se usa el primero que se pueda reconocer. 

  11. Microsoft Learn, What’s new in .NET Framework. Sobre la incorporación en .NET Framework 4.8 del soporte de Per-Monitor V2 DPI Awareness y del escalado de DPI en modo mixto para WPF, las mejoras en la interoperabilidad de HWND / WinForms alojados, y el switch de AppContext necesario para activarlo. 

  12. Microsoft Learn, TextFormattingMode Enum. Sobre los dos modos de formato de texto, Ideal (métricas ideales) y Display (métricas compatibles con GDI). 

  13. Microsoft Learn, Layout Considerations for the WindowsFormsHost Element. Sobre que WindowsFormsHost convierte entre los dos sistemas de coordenadas, DIP y píxeles físicos, y que el escalado llega solo hasta donde puede admitirlo el control de Windows Forms interior. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

Me han dicho que WPF no necesita adaptación al DPI, entonces ¿por qué se ve difuminado?
WPF utiliza como unidad de diseño el DIP (Device Independent Pixel, unidad independiente del dispositivo), equivalente a 1/96 de pulgada, y por defecto funciona como System DPI Aware, por lo que sigue correctamente el DPI del monitor principal en el momento del arranque. Sin embargo, el DPI del sistema queda fijado en el momento de iniciar sesión, de modo que al mover la ventana a un monitor con un DPI distinto, el sistema operativo amplía o reduce toda la ventana como si fuera un mapa de bits para compensar la diferencia, y el conjunto se difumina de manera uniforme. La degradación es especialmente visible en combinaciones no enteras como 125 % y 150 %. La solución definitiva consiste en declarar la compatibilidad Per-Monitor DPI en el manifiesto: a partir de .NET Framework 4.6.2, WPF reescala la ventana automáticamente.
¿Por qué en WPF una línea de 1 px se ve difuminada o con un grosor irregular?
Se debe a que, al diseñarse en DIP, los bordes de los elementos no siempre caen en una posición entera de píxel físico. En un entorno al 125 %, 1 DIP equivale a 1.25 px, de modo que los bordes que caen a mitad de píxel se dibujan semitransparentes por el suavizado de bordes (antialiasing) y se ven difuminados. El primer remedio es establecer UseLayoutRounding="True" en la Window raíz: esto redondea los valores de píxel no enteros durante el paso de diseño y se propaga a todo el árbol visual. Para los puntos que aún queden, se aplica de forma puntual SnapsToDevicePixels="True", que ajusta los bordes a los límites de píxel en el momento del dibujado. Ambas propiedades están desactivadas por defecto.
¿Por qué en WPF el texto se ve nítido pero solo los iconos se ven borrosos?
El texto se dibuja como fuente vectorial, por lo que se ve nítido en cualquier DPI, mientras que las imágenes de mapa de bits se colocan en DIP y se amplían por interpolación según el DPI. Un icono de 16×16 px se amplía a 24×24 px en un entorno al 150 % y se difumina con la interpolación predeterminada (Linear). El orden de prioridad de las soluciones es: (1) convertirlo en un recurso vectorial mediante Path, Geometry, DrawingImage o una fuente de iconos; (2) preparar mapas de bits en varias resoluciones y alternarlos según el DPI con VisualTreeHelper.GetDpi u OnDpiChanged; (3) ajustar el algoritmo de interpolación con RenderOptions.BitmapScalingMode.
¿Cómo se configura la compatibilidad Per-Monitor en WPF?
Se declara escribiendo el elemento dpiAwareness en el manifiesto de la aplicación. WPF no dispone de un ajuste de proyecto ni de un mecanismo desde código para cambiarlo, como sí ocurre con ApplicationHighDpiMode en WinForms. Los requisitos son un sistema operativo Windows 10 1607 o posterior y un destino de .NET Framework 4.6.2 o superior; una vez declarado, el framework se encarga automáticamente desde el procesamiento de WM_DPICHANGED hasta el reescalado de la ventana. La compatibilidad con PerMonitorV2 en WPF llega con .NET Framework 4.8, así que entre 4.6.2 y 4.7.2 se declara únicamente PerMonitor. Si el destino sigue siendo 4.6.1 o anterior, el seguimiento queda desactivado y hace falta habilitarlo mediante un AppContext switch. Además, el comportamiento Per-Monitor de WPF cuando se aloja en ElementHost o HwndSource no cuenta con soporte oficial.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog