Jak sprawić, by mój GUI zachowywał się dobrze, gdy skalowanie czcionek systemu Windows jest większe niż 100%

108

Wybierając duże rozmiary czcionek w panelu sterowania Windows (np. 125% lub 150%), pojawiają się problemy w aplikacji VCL, za każdym razem, gdy coś jest ustawione pikselowo.

Weź TStatusBar.Panel. Ustawiłem jego szerokość tak, aby zawierała dokładnie jedną etykietę, teraz przy dużych czcionkach etykieta „przepełnia”. Ten sam problem z innymi komponentami.

Niektóre nowe laptopy firmy Dell są już dostarczane z ustawieniem domyślnym 125%, więc podczas gdy w przeszłości ten problem był dość rzadki, teraz jest naprawdę ważny.

Co można zrobić, aby rozwiązać ten problem?

LaBracca
źródło

Odpowiedzi:

56

Uwaga: zapoznaj się z innymi odpowiedziami, ponieważ zawierają one bardzo cenne techniki. Moja odpowiedź tutaj zawiera tylko zastrzeżenia i przestrogi przed założeniem, że świadomość DPI jest łatwa.

Generalnie unikam skalowania z uwzględnieniem DPI z TForm.Scaled = True. Świadomość DPI jest dla mnie ważna tylko wtedy, gdy staje się ważna dla klientów, którzy do mnie dzwonią i są gotowi za to zapłacić. Technicznym powodem tego punktu widzenia jest to, że świadomość DPI lub nie, otwierasz okno na świat bólu. Wiele standardowych i zewnętrznych formantów VCL nie działa dobrze w trybie High DPI. Godnym uwagi wyjątkiem jest to, że części VCL, które zawijają wspólne kontrolki systemu Windows, działają wyjątkowo dobrze przy wysokiej rozdzielczości DPI. Ogromna liczba zewnętrznych i wbudowanych niestandardowych elementów sterujących Delphi VCL nie działa dobrze lub w ogóle nie działa przy wysokim DPI. Jeśli planujesz włączyć TForm.Scaled, pamiętaj, aby przetestować przy 96, 125 i 150 DPI dla każdego formularza w projekcie oraz dla każdej strony trzeciej i wbudowanej kontroli, której używasz.

Samo Delphi jest napisane w Delphi. Ma włączoną flagę świadomości High DPI dla większości formularzy, chociaż nawet tak niedawno, jak w Delphi XE2, autorzy IDE sami zdecydowali NIE włączać tej flagi manifestu świadomości High DPI. Zauważ, że w Delphi XE4 i nowszych, flaga świadomości HIGH DPI jest włączona i IDE wygląda dobrze.

Sugeruję, abyś nie używał TForm.Scaled = true (co jest wartością domyślną w Delphi, więc większość twoich formularzy ma Scaled = true) z flagami High DPI Aware (jak pokazano w odpowiedziach Davida) z Aplikacje VCL, które są tworzone przy użyciu wbudowanego projektanta formularzy Delphi.

W przeszłości próbowałem zrobić minimalną próbkę tego rodzaju złamania, jakiego można się spodziewać, gdy TForm.Scaled jest prawdą i gdy skalowanie formularzy Delphi ma usterkę. Te usterki nie zawsze są wyzwalane tylko przez wartość DPI inną niż 96. Nie byłem w stanie określić pełnej listy innych rzeczy, w tym zmian rozmiaru czcionki w systemie Windows XP. Ale ponieważ większość tych usterek pojawia się tylko w moich własnych aplikacjach, w dość skomplikowanych sytuacjach, zdecydowałem się pokazać wam pewne dowody, które możecie sami zweryfikować.

Delphi XE wygląda tak, gdy ustawisz skalowanie DPI na „Fonts @ 200%” w Windows 7, a Delphi XE2 jest podobnie uszkodzony w Windows 7 i 8, ale te usterki wydają się być naprawione jak w Delphi XE4:

wprowadź opis obrazu tutaj

wprowadź opis obrazu tutaj

Są to głównie standardowe kontrolki VCL, które działają nieprawidłowo przy wysokiej rozdzielczości DPI. Zauważ, że większość rzeczy nie została w ogóle skalowana, więc programiści Delphi IDE zdecydowali się zignorować świadomość DPI, a także wyłączyć wirtualizację DPI. Taki ciekawy wybór.

Wyłącz wirtualizację DPI tylko wtedy, gdy chcesz tego nowego dodatkowego źródła bólu i trudnych wyborów. Proponuję zostawić to w spokoju. Zwróć uwagę, że wspólne elementy sterujące systemu Windows w większości wydają się działać dobrze. Należy zauważyć, że formant eksploratora danych Delphi jest opakowaniem C # WinForms wokół standardowej wspólnej kontrolki drzewa systemu Windows. To czysta usterka Microsoft, a naprawienie jej może wymagać przepisania przez Embarcadero czystej natywnej kontroli drzewa .Net dla ich eksploratora danych lub napisania kodu właściwości DPI-Check-and-Modify w celu zmiany wysokości elementów w kontrolce. Nawet Microsoft WinForms nie radzi sobie w sposób czysty z wysoką rozdzielczością DPI, automatycznie i bez niestandardowego kodu kludge.

Aktualizacja: Interesujący fakt: chociaż środowisko delphi IDE wydaje się nie być „zwirtualizowane”, nie wykorzystuje ono manifestu treści pokazanego przez Davida do osiągnięcia „wirtualizacji innej niż DPI”. Być może używa jakiejś funkcji API w czasie wykonywania.

Aktualizacja 2: W odpowiedzi na to, jak obsługiwałbym 100% / 125% DPI, wymyśliłbym plan dwufazowy. Faza 1 polega na inwentaryzacji mojego kodu pod kątem niestandardowych formantów, które należy naprawić w celu uzyskania wysokiej rozdzielczości DPI, a następnie zaplanowaniu ich naprawienia lub wycofania. Faza 2 polegałaby na przejęciu niektórych obszarów mojego kodu, które są zaprojektowane jako formularze bez zarządzania układem i zamianie ich na formularze, które używają pewnego rodzaju zarządzania układem, aby zmiany DPI lub wysokości czcionki mogły działać bez przycinania. Podejrzewam, że ta praca nad układem „między kontrolą” byłaby znacznie bardziej złożona w większości aplikacji niż praca „wewnątrz kontroli”.

Aktualizacja: W 2016 roku najnowsza wersja Delphi 10.1 Berlin działa dobrze na mojej stacji roboczej 150 dpi.

Warren P
źródło
5
Taka funkcja API byłaby SetProcessDPIAware.
David Heffernan
2
Doskonały. Dzięki za nowy faktoid. Proponuję zmodyfikować swoją odpowiedź i zaproponować tę trasę jako jedną z możliwych. Może się zdarzyć, że klienci będą chcieli nawet skonfigurować tę opcję (wyłącz ją, jeśli nie działa).
Warren P
Ekran powitalny Delphi korzysta z wirtualizacji DPI, prawdopodobnie dlatego, że wywołanie SetDPIAware następuje po udostępnieniu formularza Splash.
Warren P
6
RAD Studio to duża mieszanka standardowych kontrolek VCL, niestandardowych kontrolek, formularzy .NET WinForm i formularzy FireMonkey. Nic dziwnego, że są problemy. I dlatego RAD Studio nie jest dobrym przykładem.
Torbins
1
Jeśli masz rację, to sam VCL ma głowę w piasku. Nawet Microsoft ma głowę w piasku. Jedynym frameworkiem, którego kiedykolwiek używałem, który wykonuje zdalnie zadowalającą pracę w tym miejscu, jest COCOA na Macu.
Warren P
64

Twoje ustawienia w pliku .dfm zostaną poprawnie przeskalowane, o ile tak Scaledjest True.

Jeśli ustawiasz wymiary w kodzie, musisz skalować je przez Screen.PixelsPerInchpodzielenie przez Form.PixelsPerInch. Użyj, MulDivaby to zrobić.

function TMyForm.ScaleDimension(const X: Integer): Integer;
begin
  Result := MulDiv(X, Screen.PixelsPerInch, PixelsPerInch);
end;

To właśnie robi platforma utrwalania formularzy, kiedy Scaledjest True.

W rzeczywistości można przedstawić przekonywujący argument za zastąpieniem tej funkcji wersją, która na stałe koduje wartość 96 jako mianownik. Pozwala to na użycie bezwzględnych wartości wymiarów i nie martwi się o zmianę znaczenia, jeśli zdarzy się zmienić skalowanie czcionki na komputerze deweloperskim i ponownie zapiszesz plik .dfm. Powodem, który ma znaczenie, jest to, że PixelsPerInchwłaściwość przechowywana w pliku .dfm to wartość komputera, na którym ostatnio zapisano plik .dfm.

const
  SmallFontsPixelsPerInch = 96;

function ScaleFromSmallFontsDimension(const X: Integer): Integer;
begin
  Result := MulDiv(X, Screen.PixelsPerInch, SmallFontsPixelsPerInch);
end;

Kontynuując temat, kolejną rzeczą, na którą należy uważać, jest to, że jeśli projekt jest tworzony na wielu komputerach z różnymi wartościami DPI, zauważysz, że skalowanie używane przez Delphi podczas zapisywania plików .dfm powoduje, że kontrolki wędrują po serii zmian. . Aby tego uniknąć, w moim miejscu pracy stosujemy ścisłą zasadę, zgodnie z którą formularze są zawsze edytowane tylko w rozdzielczości 96 dpi (100% skalowanie).

W rzeczywistości moja wersja ScaleFromSmallFontsDimensionpozwala również na uwzględnienie możliwości czcionki różniącej się w czasie wykonywania od czcionki ustawionej w czasie projektowania. Na komputerach XP formularze mojej aplikacji używają 8pt Tahoma. W systemie Vista i nowszych używany jest interfejs użytkownika 9pt Segoe. Zapewnia to jeszcze jeden stopień swobody. Skalowanie musi to uwzględniać, ponieważ zakłada się, że wartości wymiarów bezwzględnych użyte w kodzie źródłowym są odniesione do linii bazowej 8 punktów Tahomy przy 96 dpi.

Jeśli używasz obrazów lub glifów w swoim interfejsie użytkownika, one również muszą zostać przeskalowane. Typowym przykładem mogą być glify używane na paskach narzędzi i menu. Będziesz chciał dostarczyć te glify jako zasoby ikon połączone z twoim plikiem wykonywalnym. Każda ikona powinna zawierać zakres rozmiarów, a następnie w czasie wykonywania należy wybrać najbardziej odpowiedni rozmiar i załadować go do listy obrazów. Kilka szczegółów na ten temat można znaleźć tutaj: Jak ładować ikony z zasobu bez aliasów?

Inną przydatną sztuczką jest zdefiniowanie wymiarów w jednostkach względnych względem TextWidthlub TextHeight. Tak więc, jeśli chcesz, aby coś miało rozmiar około 10 pionowych linii, możesz użyć 10*Canvas.TextHeight('Ag'). To bardzo przybliżone i gotowe dane, ponieważ nie pozwalają na odstępy między wierszami i tak dalej. Jednak często wszystko, co musisz zrobić, to ustawić prawidłowe skalowanie GUI PixelsPerInch.

Powinieneś także oznaczyć swoją aplikację jako obsługującą wysokie DPI . Najlepszym sposobem na to jest skorzystanie z manifestu aplikacji. Ponieważ narzędzia do budowania Delphi nie pozwalają na dostosowywanie używanego manifestu, zmusza to do połączenia własnego zasobu manifestu.

<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <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>
</assembly>

Skrypt zasobów wygląda następująco:

1 24 "Manifest.txt"

gdzie Manifest.txtzawiera rzeczywisty manifest. Musisz również dołączyć sekcję comctl32 v6 i ustawić requestedExecutionLevelna asInvoker. Następnie połącz ten skompilowany zasób ze swoją aplikacją i upewnij się, że Delphi nie próbuje zrobić tego samego z manifestem. W nowoczesnych Delphi można to osiągnąć, ustawiając opcję projektu Runtime Themes na None.

Manifest to właściwy sposób na zadeklarowanie aplikacji, która obsługuje wysoką rozdzielczość DPI. Jeśli chcesz po prostu szybko to wypróbować, nie zmieniając manifestu, zadzwoń SetProcessDPIAware. Zrób to jako pierwszą czynność, którą robisz po uruchomieniu aplikacji. Najlepiej w jednej z wczesnych sekcji inicjalizacji jednostki lub jako pierwsza rzecz w pliku .dpr.

Jeśli nie zadeklarujesz, że Twoja aplikacja obsługuje wysokie DPI, Vista i nowsze wyrenderują ją w starszym trybie dla dowolnego skalowania czcionki powyżej 125%. To wygląda dość okropnie. Staraj się nie wpaść w tę pułapkę.

Aktualizacja DPI dla Windows 8.1 na monitor

W systemie Windows 8.1 dostępna jest teraz obsługa ustawień DPI dla poszczególnych monitorów ( http://msdn.microsoft.com/en-ca/magazine/dn574798.aspx ). Jest to duży problem w przypadku nowoczesnych urządzeń, które mogą mieć różne wyświetlacze o bardzo różnych możliwościach. Możesz mieć ekran laptopa o bardzo wysokiej rozdzielczości i zewnętrzny projektor o niskiej rozdzielczości. Wsparcie takiego scenariusza wymaga jeszcze więcej pracy niż opisano powyżej.

David Heffernan
źródło
2
To nie zawsze jest prawdą. W rzeczywistości ustawienie Scaled = true, a następnie ustawienie świadomości High DPI może również spowodować dziwne problemy w większości aplikacji Delphi. Spędziłem setki godzin, próbując zmusić moje aplikacje do pracy w wysokiej rozdzielczości i stwierdziłem, że lepiej mieć okropnie wyglądającą pikselację niż przycięte elementy sterujące, przesunięte poza ekran, dodatkowe lub brakujące paski przewijania na różnych elementach sterujących itp.
Warren P
@WarrenP Myślę, że te problemy dotyczą Twojej aplikacji. Z mojego osobistego doświadczenia wynika, że ​​moja aplikacja Delphi wyświetla i skaluje się doskonale nawet przy 200% skalowaniu czcionek.
David Heffernan
2
@WarrenP I co z tego? Całkowicie możliwe jest użycie Delphi do tworzenia aplikacji, które zachowują się lepiej niż Delphi IDE.
David Heffernan
1
Widziałem wiele okien dialogowych z ustalonymi granicami utworzonymi w Delphi 5,6,7 i ustawieniu skalowania na „prawda”. Ukrywanie ok, przyciski anulowania itp. Wydaje się, że nawet niektóre okna dialogowe w Delphi2006 zostały przez to pogryzione. Mieszanie natywnych komponentów Delphi i komponentów Windows również daje dziwne efekty. Zawsze tworzę GUI w 125% skalowaniu czcionek i ustawiam właściwość skalowania na false.
LU RD
2
Świetne rzeczy. +1 za fantastyczne informacje. Moje zdanie (nie rób tego) jest drugie pod względem ważności, jeśli chodzi o potrzebę wiedzy JAK to zrobić, kiedy chcesz to zrobić ...
Warren P
42

Należy również pamiętać, że honorowanie DPI użytkownika to tylko część Twojej prawdziwej pracy:

honorowanie rozmiaru czcionki użytkownika

Od dziesięcioleci system Windows rozwiązuje ten problem, stosując koncepcję wykonywania układu przy użyciu jednostek dialogowych , a nie pikseli. „Jednostka dialogowe” jest zdefiniowane tak, że fontu średni charakter jest

  • 4 jednostki dialogowe (dlus) szerokości i
  • 8 jednostek dialogowych (clus) wysoko

wprowadź opis obrazu tutaj

Delphi dostarcza (błędne) pojęcie Scaled, w którym formularz próbuje automatycznie dostosować się na podstawie

  • Ustawienia DPI systemu Windows użytkownika, wersety
  • ustawienie DPI na komputerze dewelopera, który jako ostatni zapisał formularz

To nie rozwiązuje problemu, gdy użytkownik używa innej czcionki niż ta, z którą zaprojektowałeś formularz, np .:

  • programista zaprojektował formularz z MS Sans Serif 8pt (gdzie średni znak to 6.21px x 13.00px96dpi)
  • użytkownik korzystający z Tahoma 8pt (gdzie przeciętny znak to 5.94px x 13.00px96 dpi)

    Tak jak w przypadku każdego, kto tworzy aplikację dla systemu Windows 2000 lub Windows XP.

lub

  • programista zaprojektował formularz z ** Tahoma 8pt * (gdzie średnia postać to 5.94px x 13.00px96dpi)
  • użytkownik korzystający z Segoe UI 9pt (gdzie przeciętny znak to6.67px x 15px )

Jako dobry programista będziesz szanować preferencje czcionek użytkownika. Oznacza to, że musisz również przeskalować wszystkie kontrolki w formularzu, aby dopasować je do nowego rozmiaru czcionki:

  • rozwiń wszystko w poziomie o 12,29% (6,67 / 5,94)
  • rozciągnij wszystko w pionie o 15,38% (15/13)

Scaled nie zrobię tego za Ciebie.

Gorzej, gdy:

  • zaprojektował swój formularz w Segoe UI 9pt ( Windows Vista, Windows 7, Windows 8)
  • użytkownik korzysta z Segoe UI 14pt (np. moje preferencje), czyli10.52px x 25px

Teraz musisz wszystko skalować

  • w poziomie o 57,72%
  • w pionie o 66,66%

Scaled nie zrobię tego za Ciebie.


Jeśli jesteś sprytny, możesz zobaczyć, jak honorowanie DPI jest nieodparte:

  • formularz zaprojektowany z Segoe UI 9pt @ 96dpi (6,67px x 15px)
  • użytkownik korzystający z Segoe UI 9pt @ 150dpi (10,52px x 25px)

Nie powinieneś patrzeć na ustawienie DPI użytkownika, powinieneś patrzeć na jego rozmiar czcionki . Dwóch użytkowników działa

  • Segoe UI 14pt @ 96dpi (10,52px x 25px)
  • Segoe UI 9pt @ 150dpi (10,52px x 25px)

używają tej samej czcionki . DPI to tylko jedna rzecz, która wpływa na rozmiar czcionki; preferencje użytkownika są inne.

StandardizeFormFont

Clovis zauważył, że odwołuję się do funkcji, StandardizeFormFontktóra naprawia czcionkę w formularzu i skaluje ją do nowego rozmiaru czcionki. Nie jest to standardowa funkcja, ale cały zestaw funkcji wykonujących proste zadanie, którego firma Borland nigdy nie wykonywała.

function StandardizeFormFont(AForm: TForm): Real;
var
    preferredFontName: string;
    preferredFontHeight: Integer;
begin
    GetUserFontPreference({out}preferredFontName, {out}preferredFontHeight);

    //e.g. "Segoe UI",     
    Result := Toolkit.StandardizeFormFont(AForm, PreferredFontName, PreferredFontHeight);
end;

Windows ma 6 różnych czcionek; w systemie Windows nie ma jednego „ustawienia czcionki”.
Ale z doświadczenia wiemy, że nasze formularze powinny być zgodne z ustawieniem Czcionka tytułu ikony

procedure GetUserFontPreference(out FaceName: string; out PixelHeight: Integer);
var
   font: TFont;
begin
   font := Toolkit.GetIconTitleFont;
   try
      FaceName := font.Name; //e.g. "Segoe UI"

      //Dogfood testing: use a larger font than we're used to; to force us to actually test it    
      if IsDebuggerPresent then
         font.Size := font.Size+1;

      PixelHeight := font.Height; //e.g. -16
   finally
      font.Free;
   end;
end;

Kiedy znamy rozmiaru czcionki będziemy skalowania formularz do otrzymujemy aktualną wysokość czcionki w formie ( w pikselach ), a skalowanie w górę przez ten czynnik.

Na przykład, jeśli ustawiam formularz na -16, a formularz jest obecnie na -11, musimy przeskalować cały formularz przez:

-16 / -11 = 1.45454%

Standaryzacja przebiega w dwóch fazach. Najpierw przeskaluj formularz według stosunku nowych: starych rozmiarów czcionek. Następnie faktycznie zmień kontrolki (rekurencyjnie), aby użyć nowej czcionki.

function StandardizeFormFont(AForm: TForm; FontName: string; FontHeight: Integer): Real;
var
    oldHeight: Integer;
begin
    Assert(Assigned(AForm));

    if (AForm.Scaled) then
    begin
        OutputDebugString(PChar('WARNING: StandardizeFormFont: Form "'+GetControlName(AForm)+'" is set to Scaled. Proper form scaling requires VCL scaling to be disabled, unless you implement scaling by overriding the protected ChangeScale() method of the form.'));
    end;

    if (AForm.AutoScroll) then
    begin
        if AForm.WindowState = wsNormal then
        begin
            OutputDebugString(PChar('WARNING: StandardizeFormFont: Form "'+GetControlName(AForm)+'" is set to AutoScroll. Form designed size will be suseptable to changes in Windows form caption height (e.g. 2000 vs XP).'));
                    if IsDebuggerPresent then
                        Windows.DebugBreak; //Some forms would like it (to fix maximizing problem)
        end;
    end;

    if (not AForm.ShowHint) then
    begin
        AForm.ShowHint := True;
        OutputDebugString(PChar('INFORMATION: StandardizeFormFont: Turning on form "'+GetControlName(AForm)+'" hints. (ShowHint := True)'));
                    if IsDebuggerPresent then
                        Windows.DebugBreak; //Some forms would like it (to fix maximizing problem)
    end;

    oldHeight := AForm.Font.Height;

    //Scale the form to the new font size
//  if (FontHeight <> oldHeight) then    For compatibility, it's safer to trigger a call to ChangeScale, since a lot of people will be assuming it always is called
    begin
        ScaleForm(AForm, FontHeight, oldHeight);
    end;

    //Now change all controls to actually use the new font
    Toolkit.StandardizeFont_ControlCore(AForm, g_ForceClearType, FontName, FontHeight,
            AForm.Font.Name, AForm.Font.Size);

    //Return the scaling ratio, so any hard-coded values can be multiplied
    Result := FontHeight / oldHeight;
end;

Oto zadanie rzeczywistego skalowania formularza. Działa wokół błędów w Form.ScaleBymetodzie własnej firmy Borland . Najpierw musi wyłączyć wszystkie zakotwiczenia w formularzu, następnie wykonać skalowanie, a następnie ponownie włączyć kotwice:

TAnchorsArray = array of TAnchors;

procedure ScaleForm(const AForm: TForm; const M, D: Integer);
var
    aAnchorStorage: TAnchorsArray;
    RectBefore, RectAfter: TRect;
    x, y: Integer;
    monitorInfo: TMonitorInfo;
    workArea: TRect;
begin
    if (M = 0) and (D = 0) then
        Exit;

    RectBefore := AForm.BoundsRect;

    SetLength(aAnchorStorage, 0);
    aAnchorStorage := DisableAnchors(AForm);
    try
        AForm.ScaleBy(M, D);
    finally
        EnableAnchors(AForm, aAnchorStorage);
    end;

    RectAfter := AForm.BoundsRect;

    case AForm.Position of
    poScreenCenter, poDesktopCenter, poMainFormCenter, poOwnerFormCenter,
    poDesigned: //i think i really want everything else to also follow the nudging rules...why did i exclude poDesigned
        begin
            //This was only nudging by one quarter the difference, rather than one half the difference
//          x := RectAfter.Left - ((RectAfter.Right-RectBefore.Right) div 2);
//          y := RectAfter.Top - ((RectAfter.Bottom-RectBefore.Bottom) div 2);
            x := RectAfter.Left - ((RectAfter.Right-RectAfter.Left) - (RectBefore.Right-RectBefore.Left)) div 2;
            y := RectAfter.Top - ((RectAfter.Bottom-RectAfter.Top)-(RectBefore.Bottom-RectBefore.Top)) div 2;
        end;
    else
        //poDesigned, poDefault, poDefaultPosOnly, poDefaultSizeOnly:
        x := RectAfter.Left;
        y := RectAfter.Top;
    end;

    if AForm.Monitor <> nil then
    begin
        monitorInfo.cbSize := SizeOf(monitorInfo);
        if GetMonitorInfo(AForm.Monitor.Handle, @monitorInfo) then
            workArea := monitorInfo.rcWork
        else
        begin
            OutputDebugString(PChar(SysErrorMessage(GetLastError)));
            workArea := Rect(AForm.Monitor.Left, AForm.Monitor.Top, AForm.Monitor.Left+AForm.Monitor.Width, AForm.Monitor.Top+AForm.Monitor.Height);
        end;

//      If the form is off the right or bottom of the screen then we need to pull it back
        if RectAfter.Right > workArea.Right then
            x := workArea.Right - (RectAfter.Right-RectAfter.Left); //rightEdge - widthOfForm

        if RectAfter.Bottom > workArea.Bottom then
            y := workArea.Bottom - (RectAfter.Bottom-RectAfter.Top); //bottomEdge - heightOfForm

        x := Max(x, workArea.Left); //don't go beyond left edge
        y := Max(y, workArea.Top); //don't go above top edge
    end
    else
    begin
        x := Max(x, 0); //don't go beyond left edge
        y := Max(y, 0); //don't go above top edge
    end;

    AForm.SetBounds(x, y,
            RectAfter.Right-RectAfter.Left, //Width
            RectAfter.Bottom-RectAfter.Top); //Height
end;

a następnie musimy rekurencyjnie użyć nowej czcionki:

procedure StandardizeFont_ControlCore(AControl: TControl; ForceClearType: Boolean;
        FontName: string; FontSize: Integer;
        ForceFontIfName: string; ForceFontIfSize: Integer);
const
    CLEARTYPE_QUALITY = 5;
var
    i: Integer;
    RunComponent: TComponent;
    AControlFont: TFont;
begin
    if not Assigned(AControl) then
        Exit;

    if (AControl is TStatusBar) then
    begin
        TStatusBar(AControl).UseSystemFont := False; //force...
        TStatusBar(AControl).UseSystemFont := True;  //...it
    end
    else
    begin
        AControlFont := Toolkit.GetControlFont(AControl);

        if not Assigned(AControlFont) then
            Exit;

        StandardizeFont_ControlFontCore(AControlFont, ForceClearType,
                FontName, FontSize,
                ForceFontIfName, ForceFontIfSize);
    end;

{   If a panel has a toolbar on it, the toolbar won't paint properly. So this idea won't work.
    if (not Toolkit.IsRemoteSession) and (AControl is TWinControl) and (not (AControl is TToolBar)) then
        TWinControl(AControl).DoubleBuffered := True;
}

    //Iterate children
    for i := 0 to AControl.ComponentCount-1 do
    begin
        RunComponent := AControl.Components[i];
        if RunComponent is TControl then
            StandardizeFont_ControlCore(
                    TControl(RunComponent), ForceClearType,
                    FontName, FontSize,
                    ForceFontIfName, ForceFontIfSize);
    end;
end;

Gdy kotwice są rekursywnie wyłączone:

function DisableAnchors(ParentControl: TWinControl): TAnchorsArray;
var
    StartingIndex: Integer;
begin
    StartingIndex := 0;
    DisableAnchors_Core(ParentControl, Result, StartingIndex);
end;


procedure DisableAnchors_Core(ParentControl: TWinControl; var aAnchorStorage: TAnchorsArray; var StartingIndex: Integer);
var
    iCounter: integer;
    ChildControl: TControl;
begin
    if (StartingIndex+ParentControl.ControlCount+1) > (Length(aAnchorStorage)) then
        SetLength(aAnchorStorage, StartingIndex+ParentControl.ControlCount+1);

    for iCounter := 0 to ParentControl.ControlCount - 1 do
    begin
        ChildControl := ParentControl.Controls[iCounter];
        aAnchorStorage[StartingIndex] := ChildControl.Anchors;

        //doesn't work for set of stacked top-aligned panels
//      if ([akRight, akBottom ] * ChildControl.Anchors) <> [] then
//          ChildControl.Anchors := [akLeft, akTop];

        if (ChildControl.Anchors) <> [akTop, akLeft] then
            ChildControl.Anchors := [akLeft, akTop];

//      if ([akTop, akBottom] * ChildControl.Anchors) = [akTop, akBottom] then
//          ChildControl.Anchors := ChildControl.Anchors - [akBottom];

        Inc(StartingIndex);
    end;

    //Add children
    for iCounter := 0 to ParentControl.ControlCount - 1 do
    begin
        ChildControl := ParentControl.Controls[iCounter];
        if ChildControl is TWinControl then
            DisableAnchors_Core(TWinControl(ChildControl), aAnchorStorage, StartingIndex);
    end;
end;

A kotwice są ponownie włączane rekurencyjnie:

procedure EnableAnchors(ParentControl: TWinControl; aAnchorStorage: TAnchorsArray);
var
    StartingIndex: Integer;
begin
    StartingIndex := 0;
    EnableAnchors_Core(ParentControl, aAnchorStorage, StartingIndex);
end;


procedure EnableAnchors_Core(ParentControl: TWinControl; aAnchorStorage: TAnchorsArray; var StartingIndex: Integer);
var
    iCounter: integer;
    ChildControl: TControl;
begin
    for iCounter := 0 to ParentControl.ControlCount - 1 do
    begin
        ChildControl := ParentControl.Controls[iCounter];
        ChildControl.Anchors := aAnchorStorage[StartingIndex];

        Inc(StartingIndex);
    end;

    //Restore children
    for iCounter := 0 to ParentControl.ControlCount - 1 do
    begin
        ChildControl := ParentControl.Controls[iCounter];
        if ChildControl is TWinControl then
            EnableAnchors_Core(TWinControl(ChildControl), aAnchorStorage, StartingIndex);
    end;
end;

Dzięki pracy nad faktyczną zmianą czcionki kontrolek pozostawiono:

procedure StandardizeFont_ControlFontCore(AControlFont: TFont; ForceClearType: Boolean;
        FontName: string; FontSize: Integer;
        ForceFontIfName: string; ForceFontIfSize: Integer);
const
    CLEARTYPE_QUALITY = 5;
var
    CanChangeName: Boolean;
    CanChangeSize: Boolean;
    lf: TLogFont;
begin
    if not Assigned(AControlFont) then
        Exit;

{$IFDEF ForceClearType}
    ForceClearType := True;
{$ELSE}
    if g_ForceClearType then
        ForceClearType := True;
{$ENDIF}

    //Standardize the font if it's currently
    //  "MS Shell Dlg 2" (meaning whoever it was opted into the 'change me' system
    //  "MS Sans Serif" (the Delphi default)
    //  "Tahoma" (when they wanted to match the OS, but "MS Shell Dlg 2" should have been used)
    //  "MS Shell Dlg" (the 9x name)
    CanChangeName :=
            (FontName <> '')
            and
            (AControlFont.Name <> FontName)
            and
            (
                (
                    (ForceFontIfName <> '')
                    and
                    (AControlFont.Name = ForceFontIfName)
                )
                or
                (
                    (ForceFontIfName = '')
                    and
                    (
                        (AControlFont.Name = 'MS Sans Serif') or
                        (AControlFont.Name = 'Tahoma') or
                        (AControlFont.Name = 'MS Shell Dlg 2') or
                        (AControlFont.Name = 'MS Shell Dlg')
                    )
                )
            );

    CanChangeSize :=
            (
                //there is a font size
                (FontSize <> 0)
                and
                (
                    //the font is at it's default size, or we're specifying what it's default size is
                    (AControlFont.Size = 8)
                    or
                    ((ForceFontIfSize <> 0) and (AControlFont.Size = ForceFontIfSize))
                )
                and
                //the font size (or height) is not equal
                (
                    //negative for height (px)
                    ((FontSize < 0) and (AControlFont.Height <> FontSize))
                    or
                    //positive for size (pt)
                    ((FontSize > 0) and (AControlFont.Size <> FontSize))
                )
                and
                //no point in using default font's size if they're not using the face
                (
                    (AControlFont.Name = FontName)
                    or
                    CanChangeName
                )
            );

    if CanChangeName or CanChangeSize or ForceClearType then
    begin
        if GetObject(AControlFont.Handle, SizeOf(TLogFont), @lf) <> 0 then
        begin
            //Change the font attributes and put it back
            if CanChangeName then
                StrPLCopy(Addr(lf.lfFaceName[0]), FontName, LF_FACESIZE);
            if CanChangeSize then
                lf.lfHeight := FontSize;

            if ForceClearType then
                lf.lfQuality := CLEARTYPE_QUALITY;
            AControlFont.Handle := CreateFontIndirect(lf);
        end
        else
        begin
            if CanChangeName then
                AControlFont.Name := FontName;
            if CanChangeSize then
            begin
                if FontSize > 0 then
                    AControlFont.Size := FontSize
                else if FontSize < 0 then
                    AControlFont.Height := FontSize;
            end;
        end;
    end;
end;

To o wiele więcej kodu, niż się spodziewałeś; wiem. Smutne jest to, że nie ma na świecie programisty Delphi, poza mną, który faktycznie poprawia ich aplikacje.

Drogi deweloperze Delphi : Ustaw czcionkę systemu Windows na Segoe UI 14pt i napraw błędną aplikację

Uwaga : każdy kod jest udostępniany w domenie publicznej. Atrybucja nie jest wymagana.

Ian Boyd
źródło
1
Dziękuję za odpowiedź, ale co sugerujesz w prawdziwym świecie? Zaimplementować ręcznie zmianę rozmiaru wszystkich formantów?
LaBracca
3
„Smutne jest to, że nie ma na świecie programisty Delphi, poza mną, który faktycznie poprawia ich aplikacje”. To bardzo aroganckie stwierdzenie, które jest błędne. Z mojej odpowiedzi: W rzeczywistości moja wersja ScaleFromSmallFontsDimension uwzględnia również możliwość zmiany czcionki formularza w czasie wykonywania od tej ustawionej w czasie projektowania. Skalowanie musi to uwzględniać, ponieważ zakłada się, że wartości wymiarów bezwzględnych użyte w kodzie źródłowym są odniesione do linii bazowej 8 punktów Tahomy przy 96 dpi. Twoja odpowiedź jest dobra, pamiętaj, +1.
David Heffernan,
1
@Ian Nie ja to powiedziałem. Brzmi jak Warren.
David Heffernan
2
To jest niesamowite, Ian. Dzięki.
Warren P
2
Niedawno natrafiłem na to pytanie i odpowiedź. Zebrałem cały kod Iana do działającej jednostki tutaj: pastebin.com/dKpfnXLc i opublikowałem o tym w Google+ tutaj: goo.gl/0ARdq9 Publikowanie tutaj na wypadek, gdyby ktoś uznał to za przydatne.
W.Prins,
11

Oto mój prezent. Funkcja, która może pomóc w poziomym pozycjonowaniu elementów w układach GUI. Darmowy dla wszystkich.

function CenterInParent(Place,NumberOfPlaces,ObjectWidth,ParentWidth,CropPercent: Integer): Integer;
  {returns formated centered position of an object relative to parent.
  Place          - P order number of an object beeing centered
  NumberOfPlaces - NOP total number of places available for object beeing centered
  ObjectWidth    - OW width of an object beeing centered
  ParentWidth    - PW width of an parent
  CropPercent    - CP percentage of safe margin on both sides which we want to omit from calculation
  +-----------------------------------------------------+
  |                                                     |
  |        +--------+       +---+      +--------+       |
  |        |        |       |   |      |        |       |
  |        +--------+       +---+      +--------+       |
  |     |              |             |            |     |
  +-----------------------------------------------------+
  |     |<---------------------A----------------->|     |
  |<-C->|<------B----->|<-----B----->|<-----B---->|<-C->|
  |                    |<-D>|
  |<----------E------------>|

  A = PW-C   B = A/NOP  C=(CP*PW)/100  D = (B-OW)/2
  E = C+(P-1)*B+D }

var
  A, B, C, D: Integer;
begin
  C := Trunc((CropPercent*ParentWidth)/100);
  A := ParentWidth - C;
  B := Trunc(A/NumberOfPlaces);
  D := Trunc((B-ObjectWidth)/2);
  Result := C+(Place-1)*B+D;
end;
avra
źródło
2
Cieszę się, że ci się podoba Warren. Miał około 15 lat, kiedy nie było dostępnych rozwiązań dla problemu, który musiałem rozwiązać. I nawet dzisiaj może zaistnieć sytuacja, w której można to zastosować. B-)
avra