Compartir a través de


Resolver incidencias de DPI

Un número creciente de dispositivos se envían con pantallas de "alta resolución". Estas pantallas suelen tener más de 200 píxeles por pulgada (ppi). Trabajar con una aplicación en estos equipos requerirá que el contenido se escale verticalmente para satisfacer las necesidades de ver el contenido a una distancia de visualización normal para el dispositivo. A partir de 2014, los dispositivos móviles, como las tabletas, los portátiles tipo concha y los teléfonos, son el objetivo principal para las pantallas de alta densidad.

Windows 8.1 y versiones posteriores contienen varias características para permitir que estas máquinas funcionen con pantallas y entornos en los que la máquina está conectada a pantallas de alta densidad y densidad estándar al mismo tiempo.

  • Windows puede permitirle escalar contenido al dispositivo mediante la opción "Hacer que el texto y otros elementos sean más grandes o más pequeños" (disponibles desde Windows XP).

  • Windows 8.1 y versiones posteriores escalarán automáticamente el contenido para que la mayoría de las aplicaciones sean coherentes cuando se muevan entre pantallas de densidades de píxeles diferentes. Cuando la pantalla principal es de alta densidad (escala del 200%) y la pantalla secundaria es de densidad estándar (100%), Windows escalará el contenido de la ventana de la aplicación automáticamente en la pantalla secundaria (mostrando 1 píxel por cada 4 píxeles representados por la aplicación).

  • Windows tendrá como valor predeterminado el escalado adecuado para la densidad de píxeles y la distancia de visualización de la pantalla (Windows 7 y versiones posteriores, configurables por OEM).

  • Windows puede escalar automáticamente el contenido hasta 250% en dispositivos nuevos que superen los 280 ppp (a partir de Windows 8.1 S14).

    Windows tiene una manera de manejar el escalado de la interfaz de usuario para aprovechar el aumento de los recuentos de píxeles. Una aplicación opta por este sistema mediante la declaración de "conciencia de DPI del sistema". El sistema escala las aplicaciones que no lo hacen. Esto puede dar lugar a una experiencia de usuario borrosa donde toda la aplicación está uniformemente estirada a nivel de píxeles. Por ejemplo:

    Problemas de PPP aproximadas

    Visual Studio opta por ser compatible con el escalado de PPP y, por lo tanto, no está "virtualizado".

    Windows (y Visual Studio) aprovechan varias tecnologías de interfaz de usuario, que tienen diferentes formas de tratar con factores de escalado establecidos por el sistema. Por ejemplo:

  • WPF mide los controles de forma independiente del dispositivo (unidades, no píxeles). La interfaz de usuario de WPF se escala automáticamente para el DPI actual.

  • Todos los tamaños de texto, sin importar el marco de interfaz de usuario, se expresan en puntos, y, por lo tanto, el sistema los trata como independientes de DPI. El texto en Win32, WinForms y WPF ya escala correctamente cuando se dibuja en el dispositivo de visualización.

  • Los diálogos y ventanas de Win32/WinForms tienen medios para habilitar una disposición que se redimensiona con el texto (por ejemplo, mediante paneles de diseño de cuadrícula, flujo y tabla). Esto permite evitar ubicaciones de píxeles codificadas de forma rígida que no se escalan cuando se aumentan los tamaños de fuente.

  • Los iconos proporcionados por el sistema o los recursos basados en métricas del sistema (por ejemplo, SM_CXICON y SM_CXSMICON) ya están ampliados.

Interfaz de usuario anterior basada en Win32 (GDI, GDI+) y WinForms

Aunque WPF ya es altamente consciente de PPP, gran parte de nuestro código basado en Win32/GDI no se escribió originalmente con reconocimiento de PPP en mente. Windows ha proporcionado API de escalado de PPP. Las correcciones de los problemas de Win32 deben usarse de forma coherente en todo el producto. Visual Studio ha proporcionado una biblioteca de clases auxiliares para evitar duplicar la funcionalidad y garantizar la coherencia en todo el producto.

Imágenes de alta resolución

Esta sección es principalmente para desarrolladores que amplían Visual Studio 2013. Para Visual Studio 2015, use el servicio de imágenes integrado en Visual Studio. También puede que tenga que admitir o tener como destino muchas versiones de Visual Studio y, por tanto, usar el servicio de imágenes en 2015 no es una opción, ya que no existe en versiones anteriores. Esta sección también es para usted.

Ampliar imágenes demasiado pequeñas

Las imágenes que son demasiado pequeñas se pueden escalar verticalmente y representar en GDI y WPF mediante algunos métodos comunes. Las clases auxiliares de PPP administradas están disponibles para integradores internos y externos de Visual Studio para abordar el escalado de iconos, mapas de bits, tiras de imágenes y listas de imágenes. Los asistentes nativos de C/C++basados en Win32 están disponibles para escalar HICON, HBITMAP, HIMAGELIST y VsUI::GdiplusImage. El escalado de un mapa de bits normalmente solo requiere un cambio de una línea después de incluir una referencia a la biblioteca auxiliar. Por ejemplo:

(WinForms) DpiHelper.LogicalToDeviceUnits(ref image);

El escalado de una lista de imágenes depende de si la lista de imágenes se ha completado en tiempo de carga o se anexa en tiempo de ejecución. Si se completa en tiempo de carga, llame a LogicalToDeviceUnits() con la lista de imágenes como haría con un mapa de bits. Cuando el código necesite cargar un mapa de bits individual antes de redactar la lista de imágenes, asegúrese de escalar el tamaño de la imagen de la lista de imágenes:

imagelist.ImageSize = DpiHelper.LogicalToDeviceUnits(imagelist.ImageSize);

En el código nativo, las dimensiones se pueden escalar al crear la lista de imágenes de la siguiente manera:

ImageList_Create(VsUI::DpiHelper::LogicalToDeviceUnitsX(16),VsUI::DpiHelper::LogicalToDeviceUnitsY(16), ILC_COLOR32|ILC_MASK, nCount, 1);

Las funciones de la biblioteca permiten especificar el algoritmo de cambio de tamaño. Al escalar imágenes que se van a colocar en listas de imágenes, asegúrese de especificar el color de fondo que se utiliza para la transparencia o use el escalado NearestNeighbor (lo que provocará distorsiones del 125% y 150%).

Consulte la DpiHelper documentación de MSDN.

En la tabla siguiente se muestran ejemplos de cómo se deben escalar las imágenes en los factores de escala de PPP correspondientes. Las imágenes con contorno naranja denotan nuestra mejor práctica a partir de Visual Studio 2013 (escala de PPP del 100%-200%).

Problemas de escalado de PPP

Problemas de diseño

Los problemas comunes de diseño se pueden evitar principalmente manteniendo puntos en la interfaz de usuario escalados y relativos entre sí en lugar de usar ubicaciones absolutas (en concreto, en unidades de píxel). Por ejemplo:

  • Las posiciones de diseño y texto deben ajustarse para tener en cuenta las imágenes aumentadas de tamaño.

  • Las columnas de las grillas deben tener los anchos ajustados para el texto ampliado.

  • También será necesario escalar los tamaños codificados de forma rígida o el espacio entre los elementos. Los tamaños que solo se basan en dimensiones de texto suelen ser correctos, ya que las fuentes se escalan verticalmente automáticamente.

    Las funciones auxiliares están disponibles en la DpiHelper clase para permitir el escalado en el eje X e Y:

  • LogicalToDeviceUnitsX/LogicalToDeviceUnitsY (las funciones permiten el escalado en el eje X/Y)

  • int space = DpiHelper.LogicalToDeviceUnitsX (10);

  • int height = VsUI::DpiHelper::LogicalToDeviceUnitsY(5);

    Hay sobrecargas de LogicalToDeviceUnits para permitir el escalado de objetos como Rectángulo, Punto y Tamaño.

Uso de la biblioteca o clase DPIHelper para escalar imágenes y el diseño

La biblioteca auxiliar de DPI de Visual Studio está disponible en formas nativas y administradas y se puede usar fuera del shell de Visual Studio por otras aplicaciones.

Para usar la biblioteca, vaya a los ejemplos de extensibilidad de VSSDK de Visual Studio y clone el ejemplo de High-DPI_Images_Icons.

En los archivos de origen, incluya VsUIDpiHelper.h y llame a las funciones estáticas de la clase VsUI::DpiHelper.

#include "VsUIDpiHelper.h"

int cxScaled = VsUI::DpiHelper::LogicalToDeviceUnitsX(cx);
VsUI::DpiHelper::LogicalToDeviceUnits(&hBitmap);

Nota:

No use las funciones auxiliares en variables estáticas de nivel de módulo o de clase. La biblioteca también utiliza variables estáticas para la sincronización de subprocesos y podrías encontrar problemas de inicialización de orden. Convierta esas estáticas en variables miembro no estáticas o encapsularlas en una función (para que se construyan en el primer acceso).

Para acceder a las funciones auxiliares de DPI desde el código administrado que se ejecutará dentro del entorno de Visual Studio:

  • El proyecto consumidor debe hacer referencia a la versión más reciente de Shell MPF. Por ejemplo:

    <Reference Include="Microsoft.VisualStudio.Shell.14.0.dll" />
    
  • Asegúrese de que el proyecto tiene referencias a System.Windows.Forms, PresentationCore y PresentationUI.

  • En el código, use el espacio de nombres Microsoft.VisualStudio.PlatformUI y llame a funciones estáticas de la clase DpiHelper. Para los tipos admitidos (puntos, tamaños, rectángulos, etc.), se proporcionan funciones de extensión que devuelven nuevos objetos escalados. Por ejemplo:

    using Microsoft.VisualStudio.PlatformUI;
    double x = DpiHelper.LogicalToDeviceUnitsX(posX);
    Point ptScaled = ptOriginal.LogicalToDeviceUnits();
    DpiHelper.LogicalToDeviceUnits(ref bitmap);
    
    

Abordar el desenfoque de las imágenes de WPF en interfaces de usuario con capacidad de ampliación

En WPF, WPF cambia automáticamente el tamaño de los mapas de bits para el nivel de zoom de PPP actual mediante un algoritmo bicúbico de alta calidad, que es el valor predeterminado. Esto funciona bien para imágenes o capturas de pantalla grandes, pero no es adecuado para los iconos de elementos de menú porque provoca una percepción de falta de definición.

Recomendaciones:

  • Para la imagen del logotipo y las ilustraciones de banners, se podría utilizar el modo predeterminado de cambio de tamaño BitmapScalingMode.

  • En el caso de los elementos de menú y las imágenes de iconografía, BitmapScalingMode se debe usar cuando no provoca otros artefactos de distorsión, para eliminar la falta de nitidez (al 200% y 300%).

  • Para niveles de zoom grandes que no sean múltiplos de 100% (por ejemplo, 250% o 350%), escalar imágenes de iconografía con interpolación bicúbica resulta en una interfaz de usuario borrosa y deslavada. Se obtiene un mejor resultado escalando primero la imagen con NearestNeighbor al múltiplo mayor de 100% (por ejemplo, 200% o 300%) y escalado con bicubo desde allí. Vea Caso especial: escalado previo de imágenes de WPF para niveles de PPP grandes para obtener más información.

    La clase DpiHelper del espacio de nombres Microsoft.VisualStudio.PlatformUI proporciona un miembro BitmapScalingMode que se puede usar para el enlace. Permitirá que el entorno de Visual Studio controle el modo de escalado de mapa de bits de manera uniforme en todo el producto, en función del factor de escalado de PPP.

    Para usarlo en XAML, agregue:

xmlns:vsui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.14.0"

<Setter Property="RenderOptions.BitmapScalingMode" Value="{x:Static vs:DpiHelper.BitmapScalingMode}" />

El shell de Visual Studio ya establece esta propiedad en ventanas y diálogos de nivel superior. La interfaz de usuario basada en WPF que se ejecuta en Visual Studio ya la heredará. Si la configuración no se propaga a tus partes específicas de la interfaz de usuario, se puede establecer en el elemento raíz de la interfaz de usuario XAML/WPF. Los lugares en los que esto sucede incluyen ventanas emergentes, en elementos con padres Win32 y ventanas de diseñador que se ejecutan fuera de proceso, como Blend.

Algunas interfaces de usuario se pueden escalar independientemente del nivel de zoom de PPP establecido por el sistema, como el editor de texto de Visual Studio y los diseñadores basados en WPF (WPF Desktop y Windows Store). En estos casos, no se debe usar DpiHelper.BitmapScalingMode. Para corregir este problema en el editor, el equipo del IDE creó una propiedad personalizada titulada RenderOptions.BitmapScalingMode. Establezca ese valor de propiedad en HighQuality o NearestNeighbor en función del nivel de zoom combinado del sistema y de la interfaz de usuario.

Caso especial: preescalado de imágenes de WPF para altos niveles de puntos por pulgada (PPP)

Para niveles de zoom muy grandes que no son múltiplos de 100% (por ejemplo, 250%, 350%, etc.), escalar las imágenes de iconografía con interpolación bicúbica da como resultado una interfaz de usuario borrosa y descolorida. La impresión de estas imágenes junto con texto nítido es casi como la de una ilusión óptica. Las imágenes parecen estar más cerca del ojo y fuera de foco en relación con el texto. El resultado del escalado a este tamaño ampliado se puede mejorar escalando primero la imagen con NearestNeighbor al múltiplo más grande de 100% (por ejemplo, 200% o 300%) y luego escalando con bicúbico para el resto (un 50% adicional).

A continuación se muestra un ejemplo de las diferencias en los resultados, donde se escala la primera imagen con el algoritmo de escalado doble mejorado 100%->200%->250%, y el segundo solo con bicubic 100%->250%.

Ejemplo de problemas de escalado doble por PPP

Para permitir que la interfaz de usuario utilice este doble escalado, será necesario modificar el marcado XAML para mostrar cada elemento Image. En los ejemplos siguientes se muestra cómo usar el escalado doble en WPF en Visual Studio mediante la biblioteca DpiHelper y Shell.12/14.

Paso 1: Escale previamente la imagen a 200%, 300%, etc. usando Nearest Neighbor.

Escalado previo de la imagen utilizando un convertidor aplicado en una vinculación o con una extensión de marcado XAML. Por ejemplo:

<vsui:DpiPrescaleImageSourceConverter x:Key="DpiPrescaleImageSourceConverter" />

<Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" Width="16" Height="16" />

<Image Source="{vsui:DpiPrescaledImage Images/Help.png}" Width="16" Height="16" />

Si la imagen también necesita tematización (la mayoría, si no todas, lo necesitarán), el marcado del código puede usar un convertidor diferente que primero realice la tematización de la imagen y luego el pre-escalado. El marcado puede usar ya sea DpiPrescaleThemedImageConverter o DpiPrescaleThemedImageSourceConverter, en función del resultado de la conversión deseado.

<vsui:DpiPrescaleThemedImageSourceConverter x:Key="DpiPrescaleThemedImageSourceConverter" />

<Image Width="16" Height="16">
  <Image.Source>
    <MultiBinding Converter="{StaticResource DpiPrescaleThemedImageSourceConverter}">
      <Binding Path="Icon" />
      <Binding Path="(vsui:ImageThemingUtilities.ImageBackgroundColor)"
               RelativeSource="{RelativeSource Self}" />
      <Binding Source="{x:Static vsui:Boxes.BooleanTrue}" />
    </MultiBinding>
  </Image.Source>
</Image>

Paso 2: Asegúrese de que el tamaño final es correcto para el PPP actual.

Dado que WPF escalará la interfaz de usuario para el PPP actual mediante la propiedad BitmapScalingMode establecida en UIElement, un control de imagen que utilice una imagen preescalada como su origen se verá dos o tres veces más grande de lo que debería. A continuación se muestran dos maneras de contrarrestar este efecto:

  • Si conoce la dimensión de la imagen original en 100%, puede especificar el tamaño exacto del control Image. Estos tamaños reflejarán el tamaño de la interfaz de usuario antes de aplicar el escalado.

    <Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" Width="16" Height="16" />
    
  • Si no se conoce el tamaño de la imagen original, se puede usar layoutTransform para reducir verticalmente el objeto Image final. Por ejemplo:

    <Image Source="{Binding Path=SelectedImage, Converter={StaticResource DpiPrescaleImageSourceConverter}}" >
        <Image.LayoutTransform>
            <ScaleTransform
                ScaleX="{x:Static vsui:DpiHelper.PreScaledImageLayoutTransformScale}"
                ScaleY="{x:Static vsui:DpiHelper.PreScaledImageLayoutTransformScale}" />
        </Image.LayoutTransform>
    </Image>
    

Habilitación de la compatibilidad con HDPI en WebOC

De forma predeterminada, los controles WebOC (como el control WebBrowser en WPF o la interfaz IWebBrowser2) no habilitan la detección y compatibilidad de HDPI. El resultado será un control incrustado con contenido de pantalla demasiado pequeño en una pantalla de alta resolución. A continuación se describe cómo habilitar la compatibilidad con valores altos de PPP en una instancia de WebOC web específica.

Implemente la interfaz IDocHostUIHandler (consulte el artículo de MSDN sobre IDocHostUIHandler:

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("BD3F23C0-D43E-11CF-893B-00AA00BDCE1A")]
public interface IDocHostUIHandler
{
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ShowContextMenu(
        [In, MarshalAs(UnmanagedType.U4)] int dwID,
        [In] POINT pt,
        [In, MarshalAs(UnmanagedType.Interface)] object pcmdtReserved,
        [In, MarshalAs(UnmanagedType.IDispatch)] object pdispReserved);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetHostInfo([In, Out] DOCHOSTUIINFO info);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ShowUI(
        [In, MarshalAs(UnmanagedType.I4)] int dwID,
        [In, MarshalAs(UnmanagedType.Interface)] object activeObject,
        [In, MarshalAs(UnmanagedType.Interface)] object commandTarget,
        [In, MarshalAs(UnmanagedType.Interface)] object frame,
        [In, MarshalAs(UnmanagedType.Interface)] object doc);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int HideUI();
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int UpdateUI();
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int EnableModeless([In, MarshalAs(UnmanagedType.Bool)] bool fEnable);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int OnDocWindowActivate([In, MarshalAs(UnmanagedType.Bool)] bool fActivate);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int OnFrameWindowActivate([In, MarshalAs(UnmanagedType.Bool)] bool fActivate);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int ResizeBorder(
        [In] COMRECT rect,
        [In, MarshalAs(UnmanagedType.Interface)] object doc,
        bool fFrameWindow);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int TranslateAccelerator(
        [In] ref MSG msg,
        [In] ref Guid group,
        [In, MarshalAs(UnmanagedType.I4)] int nCmdID);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetOptionKeyPath(
        [Out, MarshalAs(UnmanagedType.LPArray)] string[] pbstrKey,
        [In, MarshalAs(UnmanagedType.U4)] int dw);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetDropTarget(
        [In, MarshalAs(UnmanagedType.Interface)] IOleDropTarget pDropTarget,
        [MarshalAs(UnmanagedType.Interface)] out IOleDropTarget ppDropTarget);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int GetExternal([MarshalAs(UnmanagedType.IDispatch)] out object ppDispatch);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int TranslateUrl(
        [In, MarshalAs(UnmanagedType.U4)] int dwTranslate,
        [In, MarshalAs(UnmanagedType.LPWStr)] string strURLIn,
        [MarshalAs(UnmanagedType.LPWStr)] out string pstrURLOut);
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int FilterDataObject(
        IDataObject pDO,
        out IDataObject ppDORet);
    }

Opcionalmente, implemente la interfaz ICustomDoc (consulte el artículo de MSDN sobre ICustomDoc:

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("3050F3F0-98B5-11CF-BB82-00AA00BDCE0B")]
public interface ICustomDoc
{
    void SetUIHandler(IDocHostUIHandler pUIHandler);
}

Asocie la clase que implementa IDocHostUIHandler con el documento de WebOC. Si implementó la interfaz ICustomDoc anterior, tan pronto como sea válida la propiedad de documento de WebOC, conviértala en ICustomDoc; luego, llame al método SetUIHandler pasando la clase que implementa IDocHostUIHandler.

// "this" references that class that owns the WebOC control and in this case also implements the IDocHostUIHandler interface
ICustomDoc customDoc = (ICustomDoc)webBrowser.Document;
customDoc.SetUIHandler(this);

Si NO implementó la interfaz ICustomDoc, tan pronto como la propiedad del documento de WebOC sea válida, deberá convertirla a IOleObject y llamar al método SetClientSite, pasando la clase que implementa IDocHostUIHandler. Establezca la bandera DOCHOSTUIFLAG_DPI_AWARE en el DOCHOSTUIINFO que se pasa a la llamada del método GetHostInfo.

public int GetHostInfo(DOCHOSTUIINFO info)
{
    // This is what the default site provides.
    info.dwFlags = (DOCHOSTUIFLAG)0x5a74012;
    // Add the DPI flag to the defaults
    info.dwFlags |=.DOCHOSTUIFLAG.DOCHOSTUIFLAG_DPI_AWARE;
    return S_OK;
}

Esto debería ser todo lo que necesita para que su control WebOC sea compatible con HPDI.

Sugerencias

  1. Si cambia la propiedad del documento en el control WebOC, es posible que tenga que volver a asociar el documento con la clase IDocHostUIHandler.

  2. Si lo anterior no funciona, hay un problema conocido con WebOC que no detecta el cambio en el indicador de PPP. La forma más confiable de corregir esto es intercambiar el zoom óptico del WebOC, lo que significa dos llamadas con dos valores diferentes para el porcentaje de zoom. Además, si se requiere esta solución alternativa, es posible que sea necesario realizarla en cada llamada de navegación.

    // browser2 is a SHDocVw.IWebBrowser2 in this case
    // EX: Call the Exec twice with DPI%-1 and then DPI% as the zoomPercent values
    IOleCommandTarget cmdTarget = browser2.Document as IOleCommandTarget;
    if (cmdTarget != null)
    {
        object commandInput = zoomPercent;
        cmdTarget.Exec(IntPtr.Zero,
                       OLECMDID_OPTICAL_ZOOM,
                       OLECMDEXECOPT_DONTPROMPTUSER,
                       ref commandInput,
                       ref commandOutput);
    }