[VB6] Tutorial: Being DPI Aware-VBForums
Results 1 to 10 of 10

Thread: [VB6] Tutorial: Being DPI Aware

  1. #1

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    [VB6] Tutorial: Being DPI Aware

    DPI AWARENESS AND YOUR VB6 APPLICATION


    The intent of this thread is to have it be a VB6 community resource for DPI related issues and solutions.

    I started it by providing lots of information that I've discovered over the past several months. I do not have every operating system to play on, nor do I have multi-monitor systems nor rotating monitors to play with. This is where you, the VB6 community comes in. Sharing solutions is what makes this forum such a great resource. Sharing DPI-related solutions can make this thread an invaluable resource.

    I ask that you do not post questions to this thread. I ask that you post issues and solutions related to DPI. You may contradict or caveat remarks I or others have made to clarify or correct. If you have specific DPI questions, I ask you to post them in the appropriate part of the questions-related forums, and if a solution is discovered, then I ask that you come back here and share your discoveries. I will do the same.

    The references below will be updated for some time to come. As you post your discoveries, I will add them here so that people can quickly jump to a topic/area they are particularly interested in.

    If you wish for me to add a 'jump-to' link, like the ones below, for any submissions you make to this tutorial, please include the following tags around the 1st word(s) or sentence in your submission/post:
    [aTarget=abc] [/aTarget] Change abc to a somewhat unique name/marker.

    References

    Writing High-DPI Win32 Applications
    Post #2 Overview
    Post #3 shows a DPI-aware manifest entry & discusses VB controls that have issues with DPI
    Post #4 discusses issues with VB usercontrols at some DPIs
    Post #5 offers a rather simple method of scaling VB picture objects
    Post #6 discusses saving/caching values as twips vs. pixels
    Post #9 discusses incorrect values returned by ScaleX/Y when converting vbHimetric
    Post #10 discusses gdiScaling option as viable vs. no scaling options
    Last edited by LaVolpe; Jul 18th, 2017 at 02:36 PM. Reason: Fixed MSDN dead URL

  2. #2

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    Re: [VB6] Tutorial: Being DPI Aware

    DPI AWARENESS AND YOUR VB6 APPLICATION


    Terms Used

    Dots per inch (DPI). DPI = 1440 / Twips Per Pixel
    Virtualized DPI: Your application thinks it is running at 96 DPI but really isn't
    Manifest: Internal or external application manifest associated with your application
    TWIP: DPI independent value. TWIP = 1440th of an inch
    Twips Per Pixel (TPP): DPI dependent value. TPP = 1440 / DPI

    Notes whether virtualized DPI applies or not:
    a. DPI and TPP are inversely proportional. As one increases, the other decreases
    b. DPI can be considered as VB DPI and System DPI. They can be different values
    c. TPP can be considered as VB TPP and System TPP. They can be different values

    Background. Following are generic comments, not VB-specific. VB is addressed later.

    Should you be concerned with a user's DPI setting? The answer is a resounding "Yes" if you want your app to maintain a professional appearance. Without making your app DPI-aware, some controls will not resize with the form and other controls. Text and graphics may appear blurred after resizing.

    Quote Originally Posted by Microsoft (MSDN)
    Writing a DPI-aware application is the key to making a UI look consistently good across a wide variety of high-DPI display settings. An application that is not DPI-aware but is running on a high-DPI display setting can suffer from many visual artifacts, including incorrect scaling of UI elements, clipped text, and blurry images. By adding support in your application for DPI awareness, you guarantee that the presentation of your application's UI is more predictable, making it more visually appealing to users.
    A "normal" setting will be 96 DPI. Without scaling for DPI, at larger resolutions, fonts, icons, and applications in general appear smaller. With monitors nowadays, screen real estate and resolution is much greater than in recent years past. Users may opt to increase the DPI settings at higher resolutions so that these items appear larger, easier to read. Windows 7 may opt to automatically increase DPI based on the monitor's size and resolution. Previous operating systems require users to make that decision.

    Most references to DPI you will see are either a percentage or actual DPI value.
    - Convert from percent to DPI value: 96 * PercentValue / 100, i.e., 125% = 120 DPI
    - Convert from DPI value to percent: DPI / 96 * 100, i.e., 144 DPI = 150%

    Samples of various DPI scaling. In order from top to bottom: 1) 100% DPI, 2) 200% DPI virtualized, 3) 200% DPI XP-Scaling or DPI-aware
    The failure of the controls to scale/position correctly are addressed in the next posting. The option button & checkbox are crystal clear & sharp in #3. Forum scaled uploaded image & looks a tad fuzzy here

    Name:  DPI96.jpg
Views: 2069
Size:  24.1 KB
    Name:  DPI96virtualized.jpg
Views: 2088
Size:  36.5 KB
    Name:  DPI175broken.jpg
Views: 2074
Size:  36.2 KB

    DPI Scaling

    There are 2 basic types of automatic DPI scaling used by Windows, since Windows XP.

    1) XP. Fonts are scaled, they are not simply stretched but a larger font is used for display purposes. The font size in your VB controls is unchanged. This presents a much more pleasing output than say stretching the output 1.25 to 2 times its original size

    2) Vista/Win7. There are two scaling modes used that apply to all applications except DPI-aware ones

    a) XP-Style scaling. This is a system-wide setting and disables DPI virtualization. No longer available as of Win8. After Win7, your only option is virtualization if the app is not declared DPI-aware.

    b) DPI Virtualization. This is probably the worst option and applies to applications that are not DPI-aware. What happens is that your application runs under a 96 DPI environment, regardless of real DPI setting. Anything that is displayed on screen is rendered to an off-screen bitmap and stretched to the DPI scale. For an easy visualization, screen capture your app into MS Paint. Now stretch that image by 125% to 200%. That is exactly what your app will look like in DPI virtualization if it is not declared as DPI-aware or DPI vritualization is not disabled.

    DPI-aware applications can only be self-identified via application manifests or, on Vista and above, the SetProcessDPIAware API. When an application is declared DPI-aware, fonts are scaled but controls are not.

    How do you tell Windows that your application is DPI-aware? Well, on XP you simply cannot. On higher operating systems, your app can do this two ways. The customer/user can also do this two ways....

    1) If your app contains an embedded or external manifest file declaring it as DPI-aware, then DPI-virtualization will never apply to your app.

    2) Supposedly, the SetProcessDPIAware API can declare your app as DPI-aware. However, I have not tested it. Per MSDN, use of this API is highly discouraged. It has the potential to affect the various dependencies your app is using. I am also unsure as to whether this must be called before your app starts (i.e., placed in Public Sub Main) or whether it can be called at any time. Bottom line, avoid this API.

    Whether or not your app is declared as DPI-aware, the user can force it to behave in the XP style of DPI scaling. This prevents DPI virtualization. Though it is an option, I doubt many typical people would opt for this. The user can do it two ways....

    1) Right click on your app and in the compatibility tab of the properties window, opt to disable DPI scaling

    2) Open the Personalization/Screen properties window and change the DPI to a custom setting. In that window, is an option to force all apps to use XP style DPI scaling. This option disappeared in Win8.


    The Problem. More work to be DPI-aware.

    When an app declares itself DPI-aware, only fonts are scaled by the system. Controls are not and neither is the app window itself. This holds true for any window that is created manually via APIs or languages that do not have any internal scaling options. VB, on the other hand, is, more or less, DPI aware. Its control dimensions and positions are stored in TWIPs. Twips are DPI independent. How many Twips per pixel is DPI-dependent. So when the DPI changes, twips per pixel changes, and the control's size and position changes also. Most VB controls will be scaled based on the DPI setting. You have no choice in the matter, other than trying to painstakingly undo the scaling.

    So, you might ask, "where's the problem? All I need is a manifest file and good to go." Unfortunately, not all VB6 controls will be scaled. I would suspect the content of many custom usercontrols also would not be scaled correctly. Images assigned to properties are never scaled. The problem consists of four main areas: 1) let Windows know your app is DPI-aware so it doesn't put it in DPI virtualization, 2) deal with controls that do not scale, 3) graphics/fonts, and 4) API usage. The next few posts talk about these issues

    Per-Monitor DPI Awareness. This is a topic left for the adventurous. Using a manifest, Win8 or higher, you can declare your app as per-monitor DPI aware. By default, VB will use the DPI and screen dimensions of the primary monitor. Since many setups can include multiple monitors (using different DPIs) and more recent operating systems allow real-time DPI changes to the monitors (without rebooting), DPI can change while your app is running. Via manifests, you can opt to adjust your application's layout and scale based on DPI changes. This does require subclassing to receive the WM_DPICHANGED message, but also requires a whole lot more. Since VB is only DPI-aware, if you will, of the primary monitor and only at startup, not if changed later, you will have to handle scaling changes all on your own. The system will take care of the non-client 'graphics' for the most part. If you opt out of per-monitor DPI awareness, the system places your app in DPI virtualization when DPI changes.
    Last edited by LaVolpe; Dec 22nd, 2015 at 08:03 PM.

  3. #3

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    Re: [VB6] Tutorial: Being DPI Aware

    Here we will discuss some DPI-related issues with emphasis on VB.

    I. Manifests. To include DPI-awareness in your manifest, include a section like the following
    Code:
      <asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
          <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
            <dpiAware>true</dpiAware>
          </asmv3:windowsSettings>
      </asmv3:application>
    To add per-monitor awareness change <dpiAware>true</dpiAware> to <dpiAware>True/PM</dpiAware>
    Note: Win10 offers a new setting, titled: dpiAwareness. It overrides dpiAware if dpiAwareness exists and only overrides in Win10 else ignored for lower O/S.

    II: Controls negatively affected by DPI scaling. Screenshot at bottom of post highlights these issues.

    Without addressing the following issues, you can expect these problems when DPI-aware. Suggest always saving objects with ScaleMode of twips. You can change the ScaleMode during Form_Load if necessary. But you want to ensure measurements in twips & nothing else is saved in the VB project files (frm, ctl, etc).

    Image controls that have Stretch=False and Picboxes that have Autosize=True, can experience these issues because they will NOT scale.

    a. Moving from lower to higher DPI. Other controls to the right & bottom of the affected control will appear proportionally larger & further away from the affected control's edges at higher DPI settings

    b. Moving from higher to lower DPI. Worse. Controls to the right and bottom of the affected control will appear proportionally smaller & closer to the affected control. In fact, they can even overlap the affected control because its size does not scale, but the other control's positions are decreasing. Though this should never be a problem if you always design your form at 96 DPI. Don't think anyone nowadays will be running in 72 DPI (75%).

    1. VB Image Control

    Problem: When the Stretch property is false (default), the image & control will not scale to DPI. Other controls will change their size & position based on DPI. The image itself will not scale with the rest of the objects on your form. Exception: Metafiles loaded into an image control, where Stretch property is false, will scale to the screen's DPI

    Fix: Always set the image control's Stretch property to True even if actual size is desired.

    2. VB Picture Control

    Problem: When the AutoSize property is True, the control will not scale to DPI. Other controls will change their size & position based on DPI. Exception: Metafiles loaded into a picturebox control, where AutoSize property is True, will scale to the screen's DPI

    Fix: Always set the control's AutoSize property to False after you add an image. During Form_Load, resize the image to the new picturebox dimensions. The code below will honor GIF/ICO transparency, however, the picbox image is no longer GIF/ICO format after the following is executed.
    Code:
        Dim tPic As StdPicture
        With Picture1
            Set tPic = .Picture
            Set .Picture = Nothing
            .Cls
            .AutoRedraw = True
            tPic.Render (.hDC), 0, 0, _
                .ScaleX(.ScaleWidth, .ScaleMode, vbPixels), .ScaleY(.ScaleHeight, .ScaleMode, vbPixels), _
                0, tPic.Height, tPic.Width, -tPic.Height, ByVal 0&
            Set .Picture = .Image
            Set tPic = Nothing
            .AutoRedraw = False     ' remove line if not applicable
        End With
    3. Line Controls. Simply ensure the form or other object, that the line is contained on, is saved with a ScaleMode of Twips.

    4. Label Controls. If the AutoSize property is set, during Form_Load, toggle the AutoSize from True to False and back to True.

    5. ListBox Control. The IntegralHeight property default value is True. After scaling, that property can result in the listbox scaling with other controls, then snapping back to a smaller height. The IntegralHeight property restricts the height due to item font similar to how AutoSize restricts pictureboxes due to image size. Might want to consider setting this property to false.

    6. Any other control, especially non-standard VB controls. Suggest running your project at 200% DPI and see if that control is DPI aware enough. If it appears to scale ok but looks like it's not using all of its inside dimensions, see post #2 regarding resizing usercontrols. If it won't scale at all, you have 3 basic choices at that point: a) find a substitute (API version maybe) that will scale, b) create your own control, c) live with DPI virtualization; don't manifest for DPI awareness.

    III. Graphics and Fonts.
    Fonts. Use TrueType or OpenType fonts for everything. They scale much better.
    Graphics. Not much to say here. You will need to scale most graphics yourself unless a control will do it for you, i.e., Stretch property. A general purpose algorithm for scaling images. See post #5 for another option.
    NewSize = OriginalSize * CurrentDPI / 96

    Note about fonts. Whenever creating a form when DPI-awareness is applicable, the first thing you do is assign the form a TrueType font. Since many controls inherit the form's font, this can prevent you from having to individually set the font for many controls.

    IV. APIs. Many GDI APIs return virtualized settings if VB is running in virtualized DPI. If not running virtualized, you should have no real issues with GDI APIs. A common problem with some code used to get a screen capture, while virtualized, is that the screen capture is truncated; it doesn't include the entire screen. This is because when virtualized, VB reports Screen.Width,Height scaled from the current DPI to 96 DPI. Thankfully for some, not all GDI APIs report virtualized settings. The screen's actual size can be retrieved from the code shown a little bit further down the posting.

    GDI+ has some functions that are DPI related.

    GdipDrawImage will render the image using the following formula. This is the only image rendering function I know of that is DPI aware. Per MSDN, whenever the width & height of the image are not provided to the rendering function, DPI awareness applies. This is the only image rendering function that doesn't have width & height parameters:
    Formula: ScaledSize = (ImageRawSize * ScreenDPI) / EmbeddedImageDPI

    GdipGetImageFlags will return a Long value that will tell you whether or not embedded DPI exists in the image. That Long value must be bitwise compared with &H1000. If that bit is set, embedded DPI exists. Note this fails on metafiles.

    GdipGetImageHorizontalResolution returns the horizontal DPI of the image. If no embedded DPI, then the screen's DPI is returned

    GdipGetImageVerticalResolution returns the vertical DPI of the image. If no embedded DPI, then the screen's DPI is returned

    GdipBitmapSetResolution will embed DPI information into an image (except metafiles). The image must later be saved to persist the setting. Not all image formats persist that setting.

    V. VB's Screen object. This can be broken in a couple of different scenarios.

    1. I do not have a screen that can be rotated 90 degrees. From what I've read is that after a VB project loads, the Screen.Width and Screen.Height do not reflect the change after rotation. If anyone can verify this, please do. In any case, I would then assume that querying GetDeviceCaps with HORZRES & VERTRES should return the correct values, relative to being in virtualized DPI.

    2. When VB is run in virtual DPI, the Screen.Width,Height properties return virtual values. This is not normally an issue except if needing the actual screen dimensions for API related stuff.

    3. Screen.TwipsPerPixelX,TwipsPerPixelY may be incorrect. See next post for more information. The key here is that VB's TwipsPerPixel are always whole numbers. The real twips per pixel may be a fraction as seen with 200% DPI.

    To address the Screen.Width,Height problems above, one can use the following code and get dimensions via APIs. Pass the 'Actual' parameter as True if you need the actual screen dimensions, whether virtualized or not.
    Code:
    Private Declare Function GetDeviceCaps Lib "gdi32" (ByVal hDC As Long, ByVal nIndex As Long) As Long
    Private Declare Function GetDC Lib "user32" (ByVal hWnd As Long) As Long
    Private Declare Function ReleaseDC Lib "user32" (ByVal hWnd As Long, ByVal hDC As Long) As Long
    
    ' Note: When DPI virtualization exists, HORZRES,VERTRES return virtualized values
    
    Public Property Get ScreenWidth(Optional ByVal Actual As Boolean) As Single
        ' returned as Twips
        If Actual Then
            Dim hDC As Long: hDC = GetDC(0)
            ScreenWidth = GetDeviceCaps(hDC, 118) * Screen.TwipsPerPixelX ' 118=DESKTOPHORZRES
            ReleaseDC 0, hDC
        Else
            ScreenWidth = Screen.Width
        End If
        
    End Property
    
    Public Property Get ScreenHeight(Optional ByVal Actual As Boolean) As Single
        ' returned as Twips
        If Actual Then
            Dim hDC As Long: hDC = GetDC(0)
            ScreenHeight = GetDeviceCaps(hDC, 117) * Screen.TwipsPerPixelY ' 117=DESKTOPVERTRES
            ReleaseDC 0, hDC
        Else
            ScreenHeight = Screen.Height
        End If
    End Property
    
    Public Property Get ScreenDPI(Optional ByVal Actual As Boolean) As Single
        If Actual Then
            Dim hDC As Long: hDC = GetDC(0)
            ScreenDPI = GetDeviceCaps(hDC, 118) / (Screen.Width / Screen.TwipsPerPixelX)
            ReleaseDC 0, hDC
            If ScreenDPI = 1 Then
                ScreenDPI = 1440! / Screen.TwipsPerPixelX
            Else
                ScreenDPI = ScreenDPI * 96!
            End If
        Else
            ScreenDPI = 1440! / Screen.TwipsPerPixelX
        End If
    End Property
    The following code can also be used to determine if you are in DPI virtualization or not
    Code:
    If ScreenWidth(False) <> ScreenWidth(True) Then MsgBox "Virtualized DPI"
    ================================================================================

    The following screenshots highlight some VB control issues. Compare setting changes listed after the images.
    The 96 DPI form follows with some notes:
    Name:  dpi96.png
Views: 747
Size:  103.9 KB
    About the controls.
    The blue line is a VB line control
    The lion image is a metafile contained in a picturebox with AutoSize=True
    The 4 controls above the option button are:
    -- Image control Stretch=False, PicBox AutoSize=True, Image control Stretch=True, PicBox AutoSize=False
    The option button & checkbox below are crystal clear & sharp. Forum scaled uploaded image & looks a tad fuzzy here

    Name:  DPI175broken.jpg
Views: 2136
Size:  36.2 KB
    Image above is same form at 200% DPI, manifested as DPI aware. Notice a few things:
    - Line size & position is hosed. This is because the form was saved with a ScaleMode of pixels
    - The icon images did not scale, only 1 of the image & picbox controls scaled because of the Stretch & AutoSize property settings
    - The label dimensions are larger than the label text
    - The button's icon did not scale
    - The lion scaled with AutoSize=True. Metafiles scale with DPI
    - Fonts are VB defaults: not TrueType

    Name:  DPI175.jpg
Views: 2080
Size:  40.1 KB
    Image above is same, but with some tweaks made
    - Line control scaled correctly because its form was saved with ScaleMode of Twips
    - Label control's AutoSize was toggled during Form_Load
    - All 4 icon images scaled. Image controls set Stretch=True, Picbox images were manually scaled during Form_Load, AutoSize=False
    - Button's icon scaled with code in post #5
    - Lion picbox AutoSize left as True
    - Fonts are TrueType
    Last edited by LaVolpe; May 1st, 2017 at 08:18 PM.

  4. #4

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    Re: [VB6] Tutorial: Being DPI Aware

    DPI and VB created UserControls. Potential for lots of scaling issues. The following applies to DPI-aware VB apps.

    Even if you are not writing a usercontrol, this information is nice to know if you are having problems sizing a usercontrol from your form via code. It may just apply.

    Since you have no control whether the host of your usercontrol (UC) will be in a DPI-aware environment or not, you should write your UC to be DPI aware. Not suggesting it be per-monitor DPI aware. Besides the effort of making your control DPI aware, you have to be alert to the fact that VB may lie to you regarding your UC's actual size. Huh?

    When VB is running in an untypical DPI, its Twips Per Pixel (TPP) value may not be accurate. And since UC sizes are twips-based, this could be an issue. UCs are sized internally via the UserControl.Size command, along with the UC's Width & Height properties, twips are used. Here is when VB breaks. TPP is calculated simply as: 1440/DPI. Typical DPIs like 100%,125%,150% do not cause a problem, because the TPP calculation results in a whole number: 1440/96, 1440/120, 1440/144, respectively. At 200% (192 DPI), TPP would be expected to be 7.5: 1440/192 = 7.5. However, VB reports it as 7.

    The rest of this post pertains to the scenario where real TPP is not a whole number
    Here is link to a thread that discusses this issue when I first discovered it

    Unfortunately, this issue results in the host thinking the UC is one size while the UC is thinking it is another size. If both the UC and the form it is hosted on are in scalemode of Twips, then the Form's value for the UC's width should be the same as the UserControl.Width from within the UC, but they are not. The UC will render mostly fine, except that it will not fill out the dimensions you are setting from the host. In other words, when you set the Width and/or Height of the usercontrol, it is drawing smaller than it should.

    1. Getting the UC's internal dimensions same as reported by its host
    Code:
    Private Sub UserControl_Resize()
        Static isResizing As Boolean
        If Not isResizing Then
            isResizing = True
            Extender.Move Extender.Left - Screen.TwipsPerPixelX, Extender.Top - Screen.TwipsPerPixelY
            Extender.Move Extender.Left + Screen.TwipsPerPixelX, Extender.Top + Screen.TwipsPerPixelY
            isResizing = False
        End If
    End Sub
    2. Do not use UserControl.Width,Height to retrieve dimensions, instead:
    Code:
    Private Sub GetSize(WidthTwips As Single, HeightTwips As Single)
        WidthTwips = ScaleX(Extender.Width, vbContainerSize, vbTwips)
        HeightTwips = ScaleY(Extender.Height, vbContainerSize, vbTwips)
    End Sub
    3. Resizing...
    a.Internally. Do not use UserControl.Size to resize your control. It will not work correctly for untypical DPIs. Rather use something like this:
    Code:
    Private Sub SetSize(newWidthTwips As Single, newHeightTwips As Single)
        Extender.Move Extender.Left, Extender.Top, _
            ScaleX(newWidthTwips, vbTwips, vbContainerSize), _
            ScaleY(newHeightTwips, vbTwips, vbContainerSize)
    End Sub
    b.Externally. If the usercontrol appears to be faltering when rendered as described above, you can attempt to use this code to correct it. If any graphics within the control are not scaling, it is not written to be DPI aware.
    Code:
    WIth UserControl1
        .Move .Left + Screen.TwipsPerPixelX, .Top + Screen.TwipsPerPixelY, newWidth, newHeight
        .Move .Left - Screen.TwipsPerPixelX, .Top - Screen.TwipsPerPixelY
    End With
    To determine if the DPI is an issue here, a simple calculation can be performed:
    Code:
    If (1440! \ Screen.TwipsPerPixelX) < (1440! / Screen.TwipsPerPixelX) Then ' non-whole number DPI
    Edited: Do not use the above shortcut. It fails at 175% DPI. A solution like the one in previous post is more reliable.

    The following function could also be used if just wanting to know if DPI could be a problem. Since this function is bound to virtualization, it should always return True if VB is virtualized. Maybe could be named better since virutalized DPI and system DPI are only the same when system DPI is 96. However, if this function returns FALSE, you are likely to have some DPI-related problems to address.
    Code:
    Private Declare Function GetDC Lib "user32.dll" (ByVal hwnd As Long) As Long
    Private Declare Function GetDeviceCaps Lib "gdi32.dll" (ByVal hdc As Long, ByVal nIndex As Long) As Long
    Private Declare Function ReleaseDC Lib "user32.dll" (ByVal hwnd As Long, ByVal hdc As Long) As Long
    
    Public Function VbDpiSameAsOS(Optional DPI As Long) As Boolean
        Const LOGPIXELSX As Long = 88
        Dim dDC As Long
        dDC = GetDC(0)
        DPI = GetDeviceCaps(dDC, LOGPIXELSX)
        ReleaseDC 0, dDC
        VbDpiSameAsOS = ((1440 / DPI) = (1440 \ DPI))
    End Function
    To get your UC in sync with the host form, you should resize your UC from outside of your UC, not from within. In other words, you should forever abandon usage of UserControl's properties: Width, Height, ScaleWidth, ScaleHeight and the Size method. Instead, you will want to use the UserControl.Extender object to get and set dimensions.

    The Extender object exists for every VB UC. However, its properties and methods can vary in different hosts: IE, Access, Word, VB, etc. The Extender's dimensions are what your user is seeing, not necessarily what VB is reporting to your UC. The above 'fixes' can be used when VB is the host for the UC. Other non-VB hosts will have to be handled differently, if they do not support these properties, and no one solution may work for them all.

    So how does this mismatch between VB and real TPP effect me? After you keep your UC in sync with the host, it shouldn't effect you or anything you may have read from your UC's property bag. VB appears to adjust all of its measurements based on the VB TPP, not the real TPP. For example, a 34 pixel (510 twips) object, saved at 96 DPI, displayed on 200% DPI, reflects the following: 510 twips still, but not the 68 pixels one would expect at twice the DPI. What happens is that VB does not recognize 192 (200%) as the DPI, rather it uses 205.7143 (214%). That 34 pixel object is now 72.857 pixels vs. 68.
    - 1440/7.5 real TPP = 192 DPI, but VB divides by 7: 1440/7 VB TPP = 205.7143
    - 510 twips / 7.5 real TPP = 68 pixels as expected, but 510 twips / VB 7 TPP = 72.857 pixels

    Therefore if you saved a value of say 34 to your property bag in pixels or twips, using that value within your project should pose no problems because you are likely using TwipsPerPixel for scaling or ScaleX,ScaleY. Since you are using the same scale VB is using, all should be well. Take away point: Don't use real DPI, nor real TPP, in your scaling.
    Last edited by LaVolpe; Apr 30th, 2017 at 02:36 PM.

  5. #5

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    Re: [VB6] Tutorial: Being DPI Aware

    Here is a rather simple API solution to scaling VB picture objects.
    - This will not support GIF transparency.
    - Not the 'best' solution because scaling up doesn't produce best quality
    - Wouldn't suggest reusing/rescaling same image if running as per-monitor DPI aware
    - This should not be called for metafiles. If called, it returns the passed picture object. Metafiles scale correctly.

    For better quality scaling, you should have multiple-size images available either via file, within your resource file or in a resource-only DLL.

    Note: The routine requires passing the form, usercontrol, or anything with a ScaleX,ScaleY method as the ParentForm parameter. This is simply to enable usage of ScaleX,ScaleY should the code be placed in a class or module.

    API declarations
    Code:
    Private Declare Function OleCreatePictureIndirect Lib "OLEPRO32.DLL" (lpPictDesc As Any, riid As Any, ByVal fPictureOwnsHandle As Long, ipic As IPicture) As Long
    Private Declare Function CopyImage Lib "user32.dll" (ByVal handle As Long, ByVal uType As Long, ByVal cx As Long, ByVal cy As Long, ByVal flags As Long) As Long
    The Routine
    Code:
    Public Function ScaleStdPicture(ByVal thePic As StdPicture, ParentForm As Object, TwipsPerPixel As Long) As IPicture
    
        'Private Type PictDesc
        '    Size As Long
        '    Type As Long
        '    hHandle As Long
        '    lParam1 As Long      for bitmaps/WMF only
        '                         WMF = extentX, BMP = Palette handle
        '    lParam2 As Long      for WMF only: extentY
    
        'End Type
        
        Dim lpPictDesc(0 To 3) As Long  ' equivalent to a PictDesc structure
        Dim aGUID(0 To 3) As Long       ' equivalent to GUID
        Dim hImage As Long
        Dim cx As Long, cy As Long
        Const LR_COPYFROMRESOURCE As Long = &H4000
        Const LR_COPYRETURNORG As Long = &H4
        
        If thePic Is Nothing Then Exit Function
        
        On Error Resume Next
        cx = ParentForm.ScaleX(thePic.Width, vbHimetric, vbPixels)
            cx = cx * (1440 / TwipsPerPixel) / 96
        cy = ParentForm.ScaleY(thePic.Height, vbHimetric, vbPixels)
            cy = cy * (1440 / TwipsPerPixel) / 96
        If Err Then ' something's wrong, passed invalid Object?
            Err.Clear
        Else    
            Select Case thePic.Type
                Case vbPicTypeBitmap
                    hImage = CopyImage(thePic.handle, 0&, cx, cy, LR_COPYRETURNORG)
                Case vbPicTypeIcon
                    hImage = CopyImage(thePic.handle, 1&, cx, cy, LR_COPYFROMRESOURCE Or LR_COPYRETURNORG)
                    If hImage = 0& Then
                        hImage = CopyImage(thePic.handle, 1&, cx, cy, LR_COPYRETURNORG)
                    End If
                Case Else
            End Select
        End If
        On Error GoTo 0
    
        If hImage = 0& Or hImage = thePic.handle Then
            Set ScaleStdPicture = thePic
        Else
            ' fill in PictDesc structure
            lpPictDesc(0) = 16&
            lpPictDesc(1) = thePic.Type
            lpPictDesc(2) = hImage
            ' IPicture GUID {7BF80980-BF32-101A-8BBB-00AA00300CAB}
            aGUID(0) = &H7BF80980
            aGUID(1) = &H101ABF32
            aGUID(2) = &HAA00BB8B
            aGUID(3) = &HAB0C3000
        
            ' create stdPicture
            Call OleCreatePictureIndirect(lpPictDesc(0), aGUID(0), True, ScaleStdPicture)
        End If
        
    End Function
    Sample usage: Set Image1.Picture = ScaleStdPicture(Image1.Picture, Me, Screen.TwipsPerPixelX)

    Edited: If you are loading 32bpp alpha blended icons via the API LoadImage, you could use the formula above for calculating the scaled dimensions and pass those to the API also. Example of asking for a 32x32 icon scaled to VB's current DPI:
    Code:
    hIcon = LoadImage(0, filename, IMAGE_ICON, _
        32& * (1440 / Screen.TwipsPerPixelX) / 96, 32& * (1440 / Screen.TwipsPerPixelY) / 96, _
        LR_LOADFROMFILE)
    Last edited by LaVolpe; Dec 22nd, 2015 at 07:52 PM.

  6. #6

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    Re: [VB6] Tutorial: Being DPI Aware

    Let's talk a bit about caching coordinates and dimensions.

    The first question people may ask, "Do I save these as pixels or twips?" The answer is, "It's up to you". But keep these in mind...

    1. I feel it is far less effort to simply use whatever DPI that VB says it is running in, i.e., use the Screen.TwipsPerPixel property values. From the posts above, we know that at 200% DPI, VB doesn't scale everything by 2x. VB actually scales it a bit bigger. It isn't technically correct, but to compensate for this would require manually scaling nearly everything yourself. That being said, storing values as twips is far easier in the long run since they are DPI-independent.

    However, above being said... If you do not want DPI interfering with the saved value, save it in something other than Twips. For example, you may have an option to use DPI awareness in scaling. If the option is false, then save value as pixels otherwise save as twips. When the project reads that property next time it runs, the pixel value can be scaled to current TPP if no DPI is to be used; otherwise, no scaling needed because you saved it in twips. Examples by the numbers:

    - 100 pixel, 1500 twip value saved at 96 DPI: If DPI aware then save as 1500 twips else 100 pixels
    - When loaded in 144 DPI (150% of original value)
    :: If DPI aware then scaled size, as is, would be: 1500 twips / 10 TPP = 150 pixels (150%)
    :: If not DPI aware, then scale pixels: 100 pixels * 10 TPP = 1000 twips, 100 pixels (100%)

    - 100 pixel, 1000 twip value saved at 144 DPI: If DPI aware then save as 1000 twips else 100 pixels
    - When loaded in 96 DPI (66.67% of original value)
    :: If DPI aware then scaled size, as is, would be: 1000 twips / 15 TPP = 66.67 pixels (66.67%)
    :: If not DPI aware, then scale pixels: 100 pixels * 15 TPP = 1500 twips, 100 pixels (100%)

    2. Never hardcode any conversion in your code. You can see code all over the net that converts twips to pixels by dividing the twips by hardcoded 15. This only works if your app is run at 96 DPI or virtualized DPI.

    3. Never hardcode control offsets/sizes as pixels, unless you want to scale them. 10 pixels at 96 DPI is not the same proportionally at 144 DPI unless you scale the pixel value up. Likewise, never hardcode offsets as twips at 15 twips per pixel, unless you take into account the current DPI that the IDE is running in. Yes, VB6 IDE can be run in DPIs other than 96.

    4. Saving screen coordinates can be problematic. One day your app can be running in 96 DPI, another 144 DPI possibly. If you saved the position and size of your form, using pixel values, so that next time it ran, it could position & size itself same as last ran -- gonna be disappointed. However, if you saved those values as twips, much easier.

    Note about previous statement: Regardless of scalemode you saved the values in, this does not solve the problem if your form was previously positioned on a monitor that is no longer available in a multi-monitor system, or if the monitor changed rotation. Those are separate, non-DPI related, issues.

    When all said and done, there will be exceptions depending on how you plan to use the measurements you are caching/saving. The above is to get you to think about "what ifs" pertaining to potentially different DPI environments your app is designed in and/or run in.

    Tip: If you need to scale between two DPI values:
    Code:
    ' the standard formula is: newValue = oldValue * CurrentDPI / OldDPI
        oldValue = 100: CurrentDPI = 144: OldDPI = 96
        ' So, 100 * 144 / 96 = 150
    
    ' another way of doing the same thing: newValue = oldValue * OldTwipsPerPixel / NewTwipsPerPixel
        oldValue = 100: NewTwipsPerPixel = 10: OldTwipsPerPixel = 15
        ' So, 100 * 15 / 10 = 150
    At the form level, keep in mind that the screen DPI and VB's DPI may not be the same even if your project is declared DPI aware. As shown in posts #3 & #4, you can determine if this true. When this is the case, VB will not scale by the actual DPI, but its DPI (1440 / Screen.TwipsPerPixel). At 200% DPI, for example, and your form was designed at 96 DPI to show as 500x500, it will actually show at 200% DPI as 1071x1071 not 1000x1000. This is because real DPI is 200% but VB DPI is 214%. Under most circumstances this isn't an issue, but in your specific case, it just may be.
    Last edited by LaVolpe; Apr 30th, 2017 at 02:43 PM.

  7. #7
    PowerPoster
    Join Date
    Oct 2013
    Posts
    2,711

    Re: [VB6] Tutorial: Being DPI Aware

    Question about the following
    2. Do not use UserControl.Width,Height to retrieve dimensions, instead:

    Code:
    Private Sub GetSize(WidthTwips As Single, HeightTwips As Single)
        WidthTwips = ScaleX(Extender.Width, vbContainerSize, vbTwips)
        HeightTwips = ScaleY(Extender.Height, vbContainerSize, vbTwips)
    End Sub
    Is there also an alternative needed when using UserControl.ScaleWidth,ScaleHeight?

  8. #8

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    Re: [VB6] Tutorial: Being DPI Aware

    The common controls in your VB IDE toolbox may not scale properly in 200% DPI. The common controls (version 5) treeview, for example, did not. Using the double move solution in post #4 above worked. Here is a reusable, simple routine that can be added to projects for the purpose of resizing controls. Obviously only applies for controls that have the Move method (i.e., not for Line controls).
    Code:
    Private Sub pvRescaleUserControl(TheControl As Control, TwipsWidth As Single, TwipsHeight As Single, _
                                Optional TwipsLeft As Variant, Optional TwipsTop As Variant)
                                
        ' passed control's container scalemode must be in TWIPS; else modify below using ScaleX,ScaleY as needed
        With TheControl
            If IsMissing(TwipsLeft) Then TwipsLeft = .Left
            If IsMissing(TwipsTop) Then TwipsTop = .Top
            If (1440! \ Screen.TwipsPerPixelX) = (1440! / Screen.TwipsPerPixelX) Then
                .Move TwipsLeft, TwipsTop, TwipsWidth, TwipsHeight
            Else
                .Move TwipsLeft + Screen.TwipsPerPixelX, TwipsTop + Screen.TwipsPerPixelY, TwipsWidth, TwipsHeight
                .Move TwipsLeft, TwipsTop 
            End If
        End With
    
    End Sub
    sample call: pvRescaleUserControl TreeView1, newWidth, newHeight

    Edited: FYI. The 1440 & Screen.TwipsPerPixelX ratio above will fail at 175% DPI. This is because VB reports TwipsPerPixel As 8 and real TwipsPerPixel would be 8.57. But 1440 divided by 8 (both integer & real division) result in same value -- failure. Unfortunately, this means there is no fool-proof shortcut to determine when VB & System TwipsPerPixel don't match each other than comparing VB DPI to real DPI. Using a function like the one in post #3 can be useful here:
    Code:
    If ScreenDPI(True) <> ScreenDPI(False) Then ...
    or the one in post #4
    Code:
    If VbDpiSameAsOS() = False Then ...
    FYI: Following system DPI percentages should mathematically match VB's reported TPP and 175% is not in the list: 50%,75%,100%,125%,150%,250%,300%,375%
    Last edited by LaVolpe; May 3rd, 2017 at 01:21 PM.
    Insomnia is just a byproduct of, "It can't be done"

    Newbie? Novice? Bored? Spend a few minutes browsing the FAQ section of the forum.
    Read the HitchHiker's Guide to Getting Help on the Forums.
    Here is the list of TAGs you can use to format your posts
    Here are VB6 Help Files online


    {Alpha Image Control} {Memory Leak FAQ} {GDI+ Classes/Samples} {Unicode Open/Save Dialog} {Icon Organizer/Extractor}
    {VB and DPI Tutorial} {XP/Vista Manifest Creator} {UserControl Button Template} {stdPicture Render Usage}

  9. #9

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    Re: [VB6] Tutorial: Being DPI Aware

    Another oddity to report regarding using ScaleX/ScaleY to return the actual size of stdPicture images.

    Typically, we are used to converting himetric to pixels/twips like so: ScaleX(Me.Picture.Width, vbHimetric, vbPixels)
    However, in atypical DPIs like 175%, 200%, etc, that breaks too. The function still works, but does not return the actual size of the image. For example, on a 256x256 image @ 175% DPI, you'd expect the function to return 256 as the width, correct? Not a trick question. Sure, the DPI increased, but himetric units adjust with DPI. Remember, real TPP is 1440/DPI, so at 175% DPI (168 DPI), 1440/168 is 8.57 twips per pixel and VB reports 8. This means VB's DPI is not same as system DPI, and himetric units reflect real DPI. VB's ScaleX/Y is broken in this scenario. Therefore, your image dimensions via ScaleX/Y are off too. In the above case, 274 is returned at 175% DPI. That miscalculation is a big deal if you are getting image dimensions this way.

    A workaround is to forego VB's ScaleX/Y when attempting to get the image dimensions from vbHimetric units. You have a few options:
    1. Manually parse the different image formats to get the dimensions from the raw source -- not fun for most
    2. Use APIs that can give you dimensions based on the image handle.
    3. Or the simplest method: a well known formula for converting himetric to pixels. This method does require real DPI, not VB DPI. In other words, do not get the DPI like so: 1440/Screen.TwipsPerPixelX. Since VB's TPP is likely off in these atypical DPIs, so will your DPI value. If you did that, you'd get the same bad value returned by ScaleX/Y. Use GetDeviceCaps API to get the real DPI. Then apply this substitute:

    Instead of ScaleX(Me.Picture.Width, vbHimetric, vbPixels)
    Use: CLng(Me.Picture.Width / 2540! * [RealDPI]) -- [RealDPI] = 168 in this example
    Replace Width for Height to get the picture height. The above formula should return actual image dimensions. Of course, since you are running at a higher DPI, you'll want to scale them. But at least you know their real sizes to apply it to a scaling factor.
    Last edited by LaVolpe; May 6th, 2017 at 08:33 AM. Reason: changed formula to divide then multiply to prevent chance of overflow
    Insomnia is just a byproduct of, "It can't be done"

    Newbie? Novice? Bored? Spend a few minutes browsing the FAQ section of the forum.
    Read the HitchHiker's Guide to Getting Help on the Forums.
    Here is the list of TAGs you can use to format your posts
    Here are VB6 Help Files online


    {Alpha Image Control} {Memory Leak FAQ} {GDI+ Classes/Samples} {Unicode Open/Save Dialog} {Icon Organizer/Extractor}
    {VB and DPI Tutorial} {XP/Vista Manifest Creator} {UserControl Button Template} {stdPicture Render Usage}

  10. #10

    Thread Starter
    VB-aholic & Lovin' It LaVolpe's Avatar
    Join Date
    Oct 2007
    Location
    Beside Waldo
    Posts
    15,992

    Re: [VB6] Tutorial: Being DPI Aware

    With the Win10 Creators Update, the optional gdiScaling option can be used as a better option than nothing. That option when included in a manifest offers better rendering of control borders, fonts and metafiles than if no manifest scaling options were used. However, the gdiScaling option does override any other manifest scaling options that may exist. This new option is only available in Win10 v1703 and higher. This option also results in a 'better' result during dynamic DPI changes, i.e., moving to other monitors using a different DPI or when user changes their DPI without logging off.

    Ensure you use TrueType fonts for best results.

    How this works is briefly described. When the current DPI is not 100% (96 DPI), the system will have the app draw everything at the next highest DPI that is a multiple of 96, i.e., 200%, 300%, etc. Then the result is scaled down to the current DPI. Scaling something twice its size and scaling down produces better results, on average, than scaling up to a non-100% multiple. Non-font and non-metafile graphics will not be crisp, but should be better than standard virtualization. VB will be virtualized when this option is used, it will report itself in 100% DPI. This option applies whether multiple monitor scenarios exist or not.

    The manifest entry will look a bit like this:
    Code:
        <application xmlns="urn:schemas-microsoft-com:asm.v3">
            <windowsSettings>
                <gdiScaling xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">true</gdiScaling>
            </windowsSettings>
        </application>
    Edited: Here are 3 screenshots you can view for differences in quality and results. The screenshots include an app where 1) DPI scaling is performed within code, manifested as DPI-aware, 2) No manual scaling, manifested with gdiScaling option, and 3) No scaling options at all. The screenshots were taken from a desktop set at 150% DPI. The screenshots are zipped; otherwise; the rescaling by this site of the uploaded images would not provide for a true comparison between them.
    Attached Files Attached Files
    Last edited by LaVolpe; Jul 19th, 2017 at 09:14 AM.
    Insomnia is just a byproduct of, "It can't be done"

    Newbie? Novice? Bored? Spend a few minutes browsing the FAQ section of the forum.
    Read the HitchHiker's Guide to Getting Help on the Forums.
    Here is the list of TAGs you can use to format your posts
    Here are VB6 Help Files online


    {Alpha Image Control} {Memory Leak FAQ} {GDI+ Classes/Samples} {Unicode Open/Save Dialog} {Icon Organizer/Extractor}
    {VB and DPI Tutorial} {XP/Vista Manifest Creator} {UserControl Button Template} {stdPicture Render Usage}

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Featured


Click Here to Expand Forum to Full Width

Survey posted by VBForums.