From b23e6bbfc3283e316b830be1af06dddcdc22b1d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 09:48:28 +0000 Subject: [PATCH] Make the GDI font provider the default in CADability.Forms With #468 the texts get their fonts from FontProvider.Current, which is the managed TrueType reader on every platform. To keep the texts of existing Windows Forms applications exactly as before, CADability.Forms now makes its GdiFontProvider the default as soon as one of its main classes is used (CadControl, CadCanvas, CadFrame, PaintToOpenGL, PaintToGDI): their static constructors call GdiFontProvider.MakeDefault. The kernel keeps the managed provider as its own default, so nothing changes for applications without CADability.Forms (the browser, other platforms, renderers of their own). A provider the application sets through FontProvider.Current is kept, whether it is set before or after CADability.Forms is loaded, so an application can still choose the managed provider on Windows. FontProvider tells the two cases apart: the setter marks the provider as the application's choice, the new internal FontProvider.SetDefault replaces only a default. CADability.Forms reaches it through InternalsVisibleTo, so there is no new public API. Refs #412 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 --- CADability.Forms/CadCanvas.cs | 2 + CADability.Forms/CadControl.cs | 2 + CADability.Forms/CadFrame.cs | 3 + CADability.Forms/GdiFontProvider.cs | 22 ++- CADability.Forms/PaintToGDI.cs | 2 + CADability.Forms/PaintToOpenGL.cs | 3 + CADability/FontProvider.cs | 49 +++++- CADability/Properties/AssemblyInfo.cs | 2 + .../FontProviderDefaultTests.cs | 144 ++++++++++++++++++ 9 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 tests/CADability.Tests/FontProviderDefaultTests.cs diff --git a/CADability.Forms/CadCanvas.cs b/CADability.Forms/CadCanvas.cs index b37d69694..67259ff74 100644 --- a/CADability.Forms/CadCanvas.cs +++ b/CADability.Forms/CadCanvas.cs @@ -61,6 +61,8 @@ private static CADability.Substitutes.PaintEventArgs Subst(System.Windows.Forms. private string currenToolTip; private string currentCursor; private Action callbackCollapsed; + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static CadCanvas() { GdiFontProvider.MakeDefault(); } public CadCanvas() : base() { toolTip = new ToolTip(); diff --git a/CADability.Forms/CadControl.cs b/CADability.Forms/CadControl.cs index d0fdb7d58..0f38fee6d 100644 --- a/CADability.Forms/CadControl.cs +++ b/CADability.Forms/CadControl.cs @@ -23,6 +23,8 @@ public partial class CadControl : UserControl, ICommandHandler #endregion PRIVATE FIELDS + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static CadControl() { GdiFontProvider.MakeDefault(); } /// /// Default constructor /// diff --git a/CADability.Forms/CadFrame.cs b/CADability.Forms/CadFrame.cs index 8417224d5..ca8df7a5a 100644 --- a/CADability.Forms/CadFrame.cs +++ b/CADability.Forms/CadFrame.cs @@ -20,6 +20,9 @@ namespace CADability.Forms /// public class CadFrame : FrameImpl, IUIService { + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static CadFrame() { GdiFontProvider.MakeDefault(); } + #region PRIVATE FIELDS private ICommandHandler commandHandler; diff --git a/CADability.Forms/GdiFontProvider.cs b/CADability.Forms/GdiFontProvider.cs index 846e3d9ef..2875cd91d 100644 --- a/CADability.Forms/GdiFontProvider.cs +++ b/CADability.Forms/GdiFontProvider.cs @@ -4,18 +4,32 @@ using System.Drawing.Drawing2D; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; namespace CADability.Forms { /// /// A that gets the fonts from GDI and GDI+, as CADability did before issue #412: the glyph - /// outlines from , the widths and - /// the kerning from gdi32. It knows every font Windows knows (also OpenType fonts with CFF outlines) and synthesizes bold - /// and italic faces a family does not have. Set to an instance of this class to use it; - /// the default is the . + /// outlines from , the widths + /// and the kerning from gdi32. It knows every font Windows knows (also OpenType fonts with CFF outlines) and + /// synthesizes bold and italic faces a family does not have. As soon as one of the main classes of CADability.Forms is + /// used (, , , or + /// ), this provider becomes the default, so that Windows Forms applications show their texts + /// as before. An application that sets itself keeps its choice, e.g. + /// FontProvider.Current = new ManagedFontProvider(); for the same fonts as on the other platforms. /// public class GdiFontProvider : FontProvider { + private static int madeDefault; + /// + /// Makes a the default font provider, unless the application has chosen one. Called by the + /// static constructors of the main classes of CADability.Forms; only the first call does something. + /// + internal static void MakeDefault() + { + if (Interlocked.Exchange(ref madeDefault, 1) == 0) SetDefault(() => new GdiFontProvider()); + } + private readonly Dictionary<(string family, bool bold, bool italic), GdiFontFace> faces = new Dictionary<(string, bool, bool), GdiFontFace>(); private HashSet familyNames; // upper case diff --git a/CADability.Forms/PaintToGDI.cs b/CADability.Forms/PaintToGDI.cs index 6f16beaaf..939090140 100644 --- a/CADability.Forms/PaintToGDI.cs +++ b/CADability.Forms/PaintToGDI.cs @@ -65,6 +65,8 @@ public void Dispose() LazyInitializer.EnsureInitialized(ref fontFamilyNames, () => new HashSet(FontFamily.Families.Select(f => f.Name.ToUpper()))); + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static PaintToGDI() { GdiFontProvider.MakeDefault(); } public PaintToGDI(Projection projection, Graphics graphics) : this() { diff --git a/CADability.Forms/PaintToOpenGL.cs b/CADability.Forms/PaintToOpenGL.cs index b5d132510..2efd92718 100644 --- a/CADability.Forms/PaintToOpenGL.cs +++ b/CADability.Forms/PaintToOpenGL.cs @@ -28,6 +28,9 @@ internal PaintToOpenGLException(String msg) /// public class PaintToOpenGL : IPaintTo3D, IPaintTo3DFlatText { + // texts get their fonts from GDI as before issue #412, unless the application has chosen a font provider + static PaintToOpenGL() { GdiFontProvider.MakeDefault(); } + #region IPaintTo3D data public static int debugNumTriangles = 0; public static Thread MainThread = null; diff --git a/CADability/FontProvider.cs b/CADability/FontProvider.cs index 634c1c560..2336a8998 100644 --- a/CADability/FontProvider.cs +++ b/CADability/FontProvider.cs @@ -13,28 +13,63 @@ namespace CADability /// Provides the fonts for and dimensions: the names of the font families, their metrics and the outlines /// of the glyphs. The kernel gets all fonts from , which works without any user interface. The default /// is a , which reads the TrueType font files of the system itself and therefore works on - /// every platform. CADability.Forms contains a provider based on GDI (GdiFontProvider) for applications that want the - /// fonts exactly as Windows renders them. + /// every platform. CADability.Forms contains a provider based on GDI (GdiFontProvider), which gives the fonts exactly as + /// CADability showed them on Windows before; as soon as CADability.Forms is used, it makes that provider the default, + /// unless the application has set itself. /// public abstract class FontProvider { private static FontProvider current; + private static bool chosenByApplication; + private static readonly object sync = new object(); /// /// The font provider the kernel uses. Setting it clears the cached glyphs, so that texts are rebuilt with the new /// provider, like : set it before texts are painted, or rebuild the display lists of - /// the views afterwards (). + /// the views afterwards (). A provider set here is kept: + /// CADability.Forms only replaces the default with its GDI provider while the application has not set one. /// public static FontProvider Current { - get => LazyInitializer.EnsureInitialized(ref current, () => new ManagedFontProvider()); + get + { + FontProvider res = Volatile.Read(ref current); + if (res != null) return res; + lock (sync) + { + if (current == null) current = new ManagedFontProvider(); + return current; + } + } set { - current = value ?? throw new ArgumentNullException(nameof(value)); - FontCache.GlobalFontCache.Clear(); - Text.ResetFontFamilyNames(); + if (value == null) throw new ArgumentNullException(nameof(value)); + lock (sync) + { + chosenByApplication = true; + current = value; + } + ClearCaches(); } } /// + /// Makes the provider made by the default, unless the application has set + /// itself. CADability.Forms calls this to use GDI for the fonts, as CADability did before issue #412. + /// + internal static void SetDefault(Func create) + { + lock (sync) + { + if (chosenByApplication) return; + current = create(); + } + ClearCaches(); + } + private static void ClearCaches() + { // outside the lock: the caches have locks of their own, and a thread holding one of them may ask for Current + FontCache.GlobalFontCache.Clear(); + Text.ResetFontFamilyNames(); + } + /// /// The names of all font families this provider knows, e.g. for a list to choose from. /// public abstract string[] GetFamilyNames(); diff --git a/CADability/Properties/AssemblyInfo.cs b/CADability/Properties/AssemblyInfo.cs index 6fac604ac..daaf7f73e 100644 --- a/CADability/Properties/AssemblyInfo.cs +++ b/CADability/Properties/AssemblyInfo.cs @@ -3,3 +3,5 @@ [assembly: CLSCompliant(true)] // the unit tests (tests/CADability.Tests, signed with CADabilityKey.snk) may use internal classes like FontCache [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CADability.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1ad8b0ed092aad57de8d0423919856eab910629e254ad40d6de3709a87cc161162d71827d65177e862b4822364c691d32f20beb81f0f7c17690662c2b397fe7bf556ac85e9dc66c7de56f435df1c5899a22b6fde65c423c1ec4fe3e4bb316838dbc7332ff1d31995a1657f754f942b36b82787d4c9c8e4325b4fb2871bdedbb")] +// CADability.Forms makes its GDI font provider the default (FontProvider.SetDefault) +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CADability.Forms, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1ad8b0ed092aad57de8d0423919856eab910629e254ad40d6de3709a87cc161162d71827d65177e862b4822364c691d32f20beb81f0f7c17690662c2b397fe7bf556ac85e9dc66c7de56f435df1c5899a22b6fde65c423c1ec4fe3e4bb316838dbc7332ff1d31995a1657f754f942b36b82787d4c9c8e4325b4fb2871bdedbb")] diff --git a/tests/CADability.Tests/FontProviderDefaultTests.cs b/tests/CADability.Tests/FontProviderDefaultTests.cs new file mode 100644 index 000000000..89b45b19e --- /dev/null +++ b/tests/CADability.Tests/FontProviderDefaultTests.cs @@ -0,0 +1,144 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Reflection; + +namespace CADability.Tests +{ + /// + /// CADability.Forms makes its GDI font provider the default, so that Windows Forms applications show their texts as before + /// issue #412, but a provider the application has set itself is kept. The tests change the static state of + /// , so they do not run in parallel with other tests and restore it afterwards. + /// + [TestClass] + public class FontProviderDefaultTests + { + private sealed class NamedProvider : FontProvider + { + public readonly string Name; + public NamedProvider(string name) { Name = name; } + public override string[] GetFamilyNames() => new string[0]; + public override FontFace GetFace(string familyName, bool bold, bool italic) => null; + } + + private static readonly FieldInfo current = typeof(FontProvider).GetField("current", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly FieldInfo chosenByApplication = typeof(FontProvider).GetField("chosenByApplication", BindingFlags.NonPublic | BindingFlags.Static); + + /// + /// Runs as in a fresh process, where the application has not chosen a provider yet, and restores + /// the provider of the other tests afterwards. + /// + private static void AsInAFreshProcess(Action test) + { + object savedCurrent = current.GetValue(null), savedChosen = chosenByApplication.GetValue(null); + try + { + current.SetValue(null, null); + chosenByApplication.SetValue(null, false); + test(); + } + finally + { + current.SetValue(null, savedCurrent); + chosenByApplication.SetValue(null, savedChosen); + CADability.GeoObject.FontCache.GlobalFontCache.Clear(); + CADability.GeoObject.Text.ResetFontFamilyNames(); + } + } + + [TestMethod] + [DoNotParallelize] + public void TheDefaultIsTheManagedProvider() + { + AsInAFreshProcess(() => Assert.IsInstanceOfType(FontProvider.Current, typeof(ManagedFontProvider))); + } + + [TestMethod] + [DoNotParallelize] + public void AFrontEndCanReplaceTheDefault() + { + AsInAFreshProcess(() => + { + FontProvider.SetDefault(() => new NamedProvider("front end")); + Assert.AreEqual("front end", ((NamedProvider)FontProvider.Current).Name); + }); + // also after the managed default was already used + AsInAFreshProcess(() => + { + Assert.IsInstanceOfType(FontProvider.Current, typeof(ManagedFontProvider)); + FontProvider.SetDefault(() => new NamedProvider("front end")); + Assert.AreEqual("front end", ((NamedProvider)FontProvider.Current).Name); + }); + } + + [TestMethod] + [DoNotParallelize] + public void TheProviderOfTheApplicationIsKept() + { + // set before the front end is loaded + AsInAFreshProcess(() => + { + FontProvider.Current = new NamedProvider("application"); + FontProvider.SetDefault(() => new NamedProvider("front end")); + Assert.AreEqual("application", ((NamedProvider)FontProvider.Current).Name); + }); + // set after the front end is loaded + AsInAFreshProcess(() => + { + FontProvider.SetDefault(() => new NamedProvider("front end")); + FontProvider.Current = new NamedProvider("application"); + FontProvider.SetDefault(() => new NamedProvider("front end")); + Assert.AreEqual("application", ((NamedProvider)FontProvider.Current).Name); + }); + } + +#if WINDOWS + private static readonly MethodInfo makeDefault = typeof(CADability.Forms.GdiFontProvider).GetMethod("MakeDefault", BindingFlags.NonPublic | BindingFlags.Static); + + [TestMethod] + [DoNotParallelize] + public void CADabilityFormsMakesGdiTheDefault() + { + FieldInfo madeDefault = typeof(CADability.Forms.GdiFontProvider).GetField("madeDefault", BindingFlags.NonPublic | BindingFlags.Static); + object savedMadeDefault = madeDefault.GetValue(null); + try + { + AsInAFreshProcess(() => + { + madeDefault.SetValue(null, 0); + makeDefault.Invoke(null, null); + Assert.IsInstanceOfType(FontProvider.Current, typeof(CADability.Forms.GdiFontProvider)); + }); + AsInAFreshProcess(() => + { + madeDefault.SetValue(null, 0); + FontProvider.Current = new ManagedFontProvider(); + makeDefault.Invoke(null, null); + Assert.IsInstanceOfType(FontProvider.Current, typeof(ManagedFontProvider), "the choice of the application is kept"); + }); + } + finally + { + madeDefault.SetValue(null, savedMadeDefault); + } + } + + [TestMethod] + [DataRow(typeof(CADability.Forms.CadControl))] + [DataRow(typeof(CADability.Forms.CadCanvas))] + [DataRow(typeof(CADability.Forms.CadFrame))] + [DataRow(typeof(CADability.Forms.PaintToOpenGL))] + [DataRow(typeof(CADability.Forms.PaintToGDI))] + public void TheMainClassesOfFormsMakeGdiTheDefault(Type formsClass) + { + // the static constructor calls GdiFontProvider.MakeDefault: look for "call MakeDefault" in its IL + ConstructorInfo cctor = formsClass.TypeInitializer; + Assert.IsNotNull(cctor, formsClass.Name + " has no static constructor"); + byte[] il = cctor.GetMethodBody().GetILAsByteArray(); + byte[] token = BitConverter.GetBytes(makeDefault.MetadataToken); + bool found = false; + for (int i = 0; i + 4 < il.Length && !found; ++i) + found = il[i] == 0x28 && il[i + 1] == token[0] && il[i + 2] == token[1] && il[i + 3] == token[2] && il[i + 4] == token[3]; + Assert.IsTrue(found, formsClass.Name + " does not call GdiFontProvider.MakeDefault"); + } +#endif + } +}