@@ -221,9 +221,11 @@ public unsafe partial class Control :
221
221
private static readonly int s_cacheTextCountProperty = PropertyStore . CreateKey ( ) ;
222
222
private static readonly int s_cacheTextFieldProperty = PropertyStore . CreateKey ( ) ;
223
223
private static readonly int s_ambientPropertiesServiceProperty = PropertyStore . CreateKey ( ) ;
224
-
225
224
private static readonly int s_dataContextProperty = PropertyStore . CreateKey ( ) ;
226
225
226
+ private static readonly int s_currentDpiProperty = PropertyStore . CreateKey ( ) ;
227
+ private static readonly int s_formerDpiProperty = PropertyStore . CreateKey ( ) ;
228
+
227
229
private static bool s_needToLoadComCtl = true ;
228
230
229
231
// This switch determines the default text rendering engine to use by some controls that support switching rendering engine.
@@ -267,8 +269,6 @@ public unsafe partial class Control :
267
269
private short _updateCount ;
268
270
private LayoutEventArgs ? _cachedLayoutEventArgs ;
269
271
private Queue < ThreadMethodEntry > ? _threadCallbackList ;
270
- internal int _deviceDpi ;
271
- internal int _oldDeviceDpi ;
272
272
273
273
// For keeping track of our ui state for focus and keyboard cues. Using a member
274
274
// variable here because we hit this a lot
@@ -307,8 +307,13 @@ internal Control(bool autoInstallSyncContext) : base()
307
307
{
308
308
Properties = new PropertyStore ( ) ;
309
309
310
- // Initialize Dpi to the value on the primary screen, we will have the correct value when the Handle is created.
311
- _deviceDpi = _oldDeviceDpi = ScaleHelper . InitialSystemDpi ;
310
+ // Initialize Dpi to the value on the primary screen, we will have the correct
311
+ // value when the Handle is created. We use the static infra to ensure that the DPI is set
312
+ // correctly in the PropertyStore, and that it is not set to 0.
313
+ // So, this is available before any Constructor of any inheriting control runs.
314
+ Properties . AddValue (
315
+ s_currentDpiProperty ,
316
+ ScaleHelper . InitialSystemDpi ) ;
312
317
313
318
_window = new ControlNativeWindow ( this ) ;
314
319
RequiredScalingEnabled = true ;
@@ -333,7 +338,7 @@ internal Control(bool autoInstallSyncContext) : base()
333
338
// At this point, the control is not yet created, but every necessary state as well as
334
339
// the base class properties are in place, because they have been initialized by the
335
340
// static ctor of Control, which is called before all other instance constructors.
336
- InitializeControl ( _deviceDpi ) ;
341
+ InitializeControl ( ) ;
337
342
338
343
// We baked the "default default" margin and min size into CommonProperties
339
344
// so that in the common case the PropertyStore would be empty. If, however,
@@ -419,59 +424,12 @@ public Control(Control? parent, string? text, int left, int top, int width, int
419
424
}
420
425
421
426
/// <summary>
422
- /// Provides inheriting controls a dedicated early-initialization hook that is guaranteed to run
423
- /// <i>before</i> <see cref="CreateParams"/> is called by any base class constructor.
424
- /// This method enables derived controls to set styles, flags, or perform other setup that must
425
- /// occur prior to any base class logic that depends on such initialization .
427
+ /// Giving a derived control a chance to initialize its state before the
428
+ /// constructor code runs. If the control needs to access the initial or former
429
+ /// DeviceDPI, it can safely access the internal Properties CurrentDpi and
430
+ /// FormerDpi at any time .
426
431
/// </summary>
427
- /// <remarks>
428
- /// <para>
429
- /// In WinForms, the constructor call chain for controls can result in base class constructors
430
- /// invoking methods such as <see cref="CreateParams"/> or other initialization logic
431
- /// <b>before</b> the derived class's constructor body executes. This makes it impossible for
432
- /// the derived class to perform certain setup in time using only its constructor.
433
- /// </para>
434
- /// <para>
435
- /// For example, consider the following inheritance and call chain:
436
- /// </para>
437
- /// <code>
438
- /// public class MyButton : Button
439
- /// {
440
- /// public MyButton()
441
- /// {
442
- /// // This code runs after Button's constructor, which may have already called CreateParams.
443
- /// }
444
- ///
445
- /// protected override void InitializeControl(int deviceDpi)
446
- /// {
447
- /// // This code runs before any base class calls to CreateParams or similar methods.
448
- /// }
449
- /// }
450
- ///
451
- /// // Call chain:
452
- /// new MyButton()
453
- /// → Control..ctor()
454
- /// → Control.InitializeControl (called in base constructor)
455
- /// → ButtonBase.InitializeControl (called before CreateParams)
456
- /// → Button.InitializeControl (called before CreateParams)
457
- /// → MyButton.InitializeControl (called before CreateParams)
458
- /// → Control.CreateParams (called in base constructor)
459
- /// → ButtonBase.CreateParams (called before CreateParams)
460
- /// → Button.CreateParams (called before CreateParams)
461
- /// → MyButton.CreateParams (called before CreateParams)
462
- /// → MyButton.CreateParams (called before CreateParams)
463
- /// → ButtonBase..ctor()
464
- /// → Button..ctor()
465
- /// → MyButton..ctor()
466
- /// </code>
467
- /// <para>
468
- /// By overriding <c>InitializeControl</c>, inheritors can ensure their initialization logic
469
- /// runs at the correct time, even when base class constructors invoke methods that require
470
- /// early setup.
471
- /// </para>
472
- /// </remarks>
473
- /// <param name="deviceDpi">The DPI value for the control's device context.</param>
474
- protected virtual void InitializeControl ( int deviceDpi )
432
+ private protected virtual void InitializeControl ( )
475
433
{
476
434
}
477
435
@@ -480,6 +438,42 @@ protected virtual void InitializeControl(int deviceDpi)
480
438
/// </summary>
481
439
internal DPI_AWARENESS_CONTEXT DpiAwarenessContext => _window . DpiAwarenessContext ;
482
440
441
+ internal int DeviceDpiInternal
442
+ {
443
+ get
444
+ {
445
+ return Properties . GetValueOrDefault (
446
+ s_currentDpiProperty ,
447
+ ScaleHelper . InitialSystemDpi ) ;
448
+ }
449
+
450
+ set
451
+ {
452
+ if ( value != DeviceDpiInternal )
453
+ {
454
+ Properties . AddOrRemoveValue ( s_currentDpiProperty , value , ScaleHelper . InitialSystemDpi ) ;
455
+ }
456
+ }
457
+ }
458
+
459
+ internal int FormerDeviceDpi
460
+ {
461
+ get
462
+ {
463
+ return Properties . GetValueOrDefault (
464
+ s_formerDpiProperty ,
465
+ DeviceDpiInternal ) ;
466
+ }
467
+
468
+ set
469
+ {
470
+ if ( value != FormerDeviceDpi )
471
+ {
472
+ Properties . AddOrRemoveValue ( s_formerDpiProperty , value , DeviceDpiInternal ) ;
473
+ }
474
+ }
475
+ }
476
+
483
477
/// <summary>
484
478
/// The Accessibility Object for this Control
485
479
/// </summary>
@@ -1666,7 +1660,7 @@ public static Font DefaultFont
1666
1660
[ DesignerSerializationVisibility ( DesignerSerializationVisibility . Hidden ) ]
1667
1661
public int DeviceDpi
1668
1662
// deviceDpi may change in WmDpiChangedBeforeParent in PmV2 scenarios, so we can't cache statically.
1669
- => ScaleHelper . IsThreadPerMonitorV2Aware ? _deviceDpi : ScaleHelper . InitialSystemDpi ;
1663
+ => ScaleHelper . IsThreadPerMonitorV2Aware ? DeviceDpiInternal : ScaleHelper . InitialSystemDpi ;
1670
1664
1671
1665
// The color to use when drawing disabled text. Normally we use BackColor,
1672
1666
// but that obviously won't work if we're transparent.
@@ -5375,9 +5369,9 @@ Site is { } site
5375
5369
// We would need to get adornments metrics for both (old and new) Dpi in case application is in PerMonitorV2 mode and Dpi changed.
5376
5370
AdjustWindowRectExForControlDpi ( ref adornmentsAfterDpiChange , ( WINDOW_STYLE ) cp . Style , bMenu : false , ( WINDOW_EX_STYLE ) cp . ExStyle ) ;
5377
5371
5378
- if ( _oldDeviceDpi != _deviceDpi && OsVersion . IsWindows10_1703OrGreater ( ) )
5372
+ if ( FormerDeviceDpi != DeviceDpiInternal && OsVersion . IsWindows10_1703OrGreater ( ) )
5379
5373
{
5380
- AdjustWindowRectExForDpi ( ref adornmentsBeforeDpiChange , ( WINDOW_STYLE ) cp . Style , bMenu : false , ( WINDOW_EX_STYLE ) cp . ExStyle , _oldDeviceDpi ) ;
5374
+ AdjustWindowRectExForDpi ( ref adornmentsBeforeDpiChange , ( WINDOW_STYLE ) cp . Style , bMenu : false , ( WINDOW_EX_STYLE ) cp . ExStyle , FormerDeviceDpi ) ;
5381
5375
}
5382
5376
else
5383
5377
{
@@ -5530,7 +5524,7 @@ internal Control[] GetChildControlsInTabOrder(bool handleCreatedOnly)
5530
5524
/// <returns>The control's <see cref="Font"/></returns>
5531
5525
private Font GetCurrentFontAndDpi ( out int fontDpi )
5532
5526
{
5533
- fontDpi = _deviceDpi ;
5527
+ fontDpi = DeviceDpiInternal ;
5534
5528
5535
5529
// If application is in PerMonitorV2 mode and font is scaled when moved between monitors.
5536
5530
if ( ScaledControlFont is not null )
@@ -6431,7 +6425,7 @@ public void ScaleBitmapLogicalToDevice(ref Bitmap logicalBitmap)
6431
6425
6432
6426
private protected void AdjustWindowRectExForControlDpi ( ref RECT rect , WINDOW_STYLE style , bool bMenu , WINDOW_EX_STYLE exStyle )
6433
6427
{
6434
- AdjustWindowRectExForDpi ( ref rect , style , bMenu , exStyle , _deviceDpi ) ;
6428
+ AdjustWindowRectExForDpi ( ref rect , style , bMenu , exStyle , DeviceDpiInternal ) ;
6435
6429
}
6436
6430
6437
6431
private static void AdjustWindowRectExForDpi ( ref RECT rect , WINDOW_STYLE style , bool bMenu , WINDOW_EX_STYLE exStyle , int dpi )
@@ -7404,19 +7398,20 @@ void HandleHighDpi()
7404
7398
return ;
7405
7399
}
7406
7400
7407
- int old = _deviceDpi ;
7401
+ int old = DeviceDpiInternal ;
7408
7402
Font localFont = GetCurrentFontAndDpi ( out int fontDpi ) ;
7409
- _deviceDpi = ( int ) PInvoke . GetDpiForWindow ( this ) ;
7410
- if ( old == _deviceDpi )
7403
+
7404
+ Properties . AddOrRemoveValue ( s_currentDpiProperty , ( int ) PInvoke . GetDpiForWindow ( this ) ) ;
7405
+ if ( old == DeviceDpiInternal )
7411
7406
{
7412
7407
return ;
7413
7408
}
7414
7409
7415
- if ( fontDpi != _deviceDpi )
7410
+ if ( fontDpi != DeviceDpiInternal )
7416
7411
{
7417
7412
// Controls are by default font scaled.
7418
7413
// Dpi change requires font to be recalculated in order to get controls scaled with right dpi.
7419
- Font fontForDpi = GetScaledFont ( localFont , _deviceDpi , fontDpi ) ;
7414
+ Font fontForDpi = GetScaledFont ( localFont , DeviceDpiInternal , fontDpi ) ;
7420
7415
ScaledControlFont = fontForDpi ;
7421
7416
7422
7417
// If it is a container control that inherit Font and is scaled by parent, we simply scale Font
@@ -7428,7 +7423,7 @@ void HandleHighDpi()
7428
7423
}
7429
7424
}
7430
7425
7431
- RescaleConstantsForDpi ( old , _deviceDpi ) ;
7426
+ RescaleConstantsForDpi ( old , DeviceDpiInternal ) ;
7432
7427
7433
7428
// If the control is top-level window and its StartPosition is not WindowsDefaultLocation, Location needs
7434
7429
// recalculated. For example, a Form centered as FormStartPosition.CenterParent or FormStartPosition.CenterScreen,
@@ -11516,7 +11511,7 @@ private void WmDpiChangedBeforeParent(ref Message m)
11516
11511
{
11517
11512
DefWndProc ( ref m ) ;
11518
11513
11519
- _oldDeviceDpi = _deviceDpi ;
11514
+ Properties . AddOrRemoveValue ( s_formerDpiProperty , DeviceDpiInternal ) ;
11520
11515
11521
11516
// In order to support tests, will be querying Dpi from the message first.
11522
11517
int newDeviceDpi = ( short ) m . WParamInternal . LOWORD ;
@@ -11527,16 +11522,16 @@ private void WmDpiChangedBeforeParent(ref Message m)
11527
11522
newDeviceDpi = ( int ) PInvoke . GetDpiForWindow ( this ) ;
11528
11523
}
11529
11524
11530
- if ( _oldDeviceDpi == newDeviceDpi )
11525
+ if ( FormerDeviceDpi == newDeviceDpi )
11531
11526
{
11532
11527
OnDpiChangedBeforeParent ( EventArgs . Empty ) ;
11533
11528
return ;
11534
11529
}
11535
11530
11536
11531
Font localFont = GetCurrentFontAndDpi ( out int fontDpi ) ;
11537
- _deviceDpi = newDeviceDpi ;
11532
+ Properties . AddOrRemoveValue ( s_currentDpiProperty , newDeviceDpi ) ;
11538
11533
11539
- if ( fontDpi == _deviceDpi )
11534
+ if ( fontDpi == DeviceDpiInternal )
11540
11535
{
11541
11536
OnDpiChangedBeforeParent ( EventArgs . Empty ) ;
11542
11537
return ;
@@ -11548,7 +11543,7 @@ private void WmDpiChangedBeforeParent(ref Message m)
11548
11543
ContainerControl ? container = this as ContainerControl ;
11549
11544
bool isLocalFontSet = IsFontSet ( ) ;
11550
11545
11551
- ScaledControlFont = GetScaledFont ( localFont , _deviceDpi , fontDpi ) ;
11546
+ ScaledControlFont = GetScaledFont ( localFont , DeviceDpiInternal , fontDpi ) ;
11552
11547
11553
11548
if ( isLocalFontSet || container is null || ! IsScaledByParent ( this ) )
11554
11549
{
@@ -11567,7 +11562,7 @@ private void WmDpiChangedBeforeParent(ref Message m)
11567
11562
// This flag is reset when scaling is done on Container in "OnParentFontChanged".
11568
11563
container ? . IsDpiChangeScalingRequired = true;
11569
11564
11570
- RescaleConstantsForDpi ( _oldDeviceDpi , _deviceDpi ) ;
11565
+ RescaleConstantsForDpi ( FormerDeviceDpi , DeviceDpiInternal ) ;
11571
11566
}
11572
11567
11573
11568
OnDpiChangedBeforeParent ( EventArgs . Empty ) ;
0 commit comments