Jump to content

Zoë Peterson

Members
  • Content Count

    15
  • Joined

  • Last visited

  • Days Won

    2

Posts posted by Zoë Peterson


  1. Sure, here's a trivial example.  Compile/install the SsExample.bpl, which adds a new minimal TSsControl.  SsProj.dproj already has a form with the relevant control.  If you make any change at all to it, you'll get the error.  If you delete the existing CtrlInit and add a new one, it'll switch to the generic declaration.

     

    This isn't restricted to something as simple  as TArray<string> though.  If the argument is a much more complicated generics declaration (e,g. TDictionary<string, TDictionary<Integer, Integer>>), that gets exploded out too.  Why would that be preferable to using a named alias?

    DynArrayFailure.zip


  2. I have some visual components that have event handlers that are declared like so:

    TSsInitEvent = procedure(Sender: TObject; var AStrs: TStringDynArray) of object;

    In the units that are using them, they're declared like:

    procedure CommandControlInit(Sender: TObject; var AStrs: TStringDynArray);

    Ctrl+Clicking shows that TStringDynArray is a direct reference to System.Types.TStringDynArray in both cases.  In the older releases, TStringDynArray was "array of string", but it's apparently now been changed to "TStringDynArray = TArray<string>".

     

    That worked just fine in older Delphi releases, but in 11.3 whenever I save the form I get the error "The CommandControlInit method referenced by CommandControl.OnInit has an incompatible parameter list.  Remove the reference?"

     

    If I try to create a new event for the relevant property, it's created like so:

    procedure CommandControlInit(Sender: TObject; var AStrs: TArray<System.string>);

    Aside from just being a more obnoxious declaration, that doesn't work for projects that also need to compile in older Delphi releases.

     

    Has anyone else run into this?  Any good workarounds?  Any open bug reports about it I can vote for?


  3. On 3/28/2022 at 5:52 PM, Zoë Peterson said:

    A better fix is to instead remove TScrollBox.WMMouseWheel entirely (if you're on Delphi 11) and put the code in a newly overridden DefaultHandler:

    I can't edit my previous message anymore, but after some further testing/research, overriding TScrollBox.MouseWheelHandler is as good of a place as DefaultHandler, and a bit more obvious.  In that case the code is identical to the first block I posted for TScrollBox.WMMouseWheel.

     


  4. I just spent an unreasonable amount of time looking into this myself 😄

     

    1) The stock TScrollBox didn't support reacting to the mouse wheel.  That changed in Delphi 11, but they did a poor job of it and broke it so it kind of scrolls, but TScrollBox no longer calls the DoMouseWheel/OnMouseWheel events. [RSP-35333] (My comment there is outdated compared to the rest of this post)

    2) The WM_MOUSEWHEEL messages used to require focus.  In a recent Windows 10 update (and Windows 11), Microsoft introduced a new mouse setting, "Scroll inactive windows when I hover over them" that instead sends the message to the control that the mouse is over.  AFAICT it's turned on by default now.  If it's turned off it reverts to the old behavior of sending it to the focused control.

    3) Delphi used to forward the CM_MOUSEWHEEL messages to the focused control in TCustomForm.MouseWheelHandler, which meant that it could still require focus even if Windows was sending the messages elsewhere.  That code was removed sometime between XE7 and 10.4, so the CM_MOUSEWHEEL wheels now go to the control the message is sent to.

    4) TControl.IsControlMouseMsg incorrectly treats the X/Y coordinates for WM_MOUSEWHEEL events as if they're in client coordinates like the other WM_MOUSE* events are, rather than in screen space coordinates.  This has the effect that if you have a TScrollBox with a big TImage on it, whether the scrollbox scrolls or not depends on where the form is on the screen.  Likewise, TControl.IsControlMouseMessage doesn't pay attention to the WM_MOUSEWHEEL return values, so if you fix the first issue, it actually breaks TScrollBox scrolling if it's over the image because the image swallows the WM_MOUSEWHEEL message and TScrollBox's handler never gets called.

    5) If you don't have TScrollBox.VertScrollBar.Smooth := true, as you're scrolling there will be a smear as the nested controls don't redraw before the scrollbox scrolls again.  It will clear up once the apps been idle a bit, and for simple scrolls this isn't noticeable, but it can look terrible, especially if the scrollbox redraw slowly (e.g., lots of nested controls with VCL styles enabled) and you use a free-spinning mouse.

    6) Once you fix all of the above, if you have scrollable controls nested in a TScrollBox, the lack of focus requirement means that if the mouse cursor goes over the top of the nested control as a result of the TScrollBox's scroll, it'll start scrolling the nested one instead.  I can provide a workaround for that, but it's a little out of scope of just "Fix the bugs".

     

    On to the fixes!

     

    1) Patch VCL.Forms.pas (fixes issue #1). 

     

    The least invasive fix is to change TScrollBox's WMMouseWheel handler to this, which at least fixes the OnMouseWheel handlders getting called.

     

    procedure TScrollBox.WMMouseWheel(var Message: TWMMouseWheel);
    const
      LScrollBarValues: array[Boolean] of WPARAM = (SB_LINEDOWN, SB_LINEUP);
    begin
      inherited;
      if (Message.Result <> 0) or not VertScrollBar.IsScrollBarVisible then
        Exit;
      Message.Result := 1;
      var LScrollBarValue := LScrollBarValues[Message.WheelDelta > 0];
      for var LLineToScroll := 1 to Mouse.WheelScrollLines do
        Perform(WM_VSCROLL, LScrollBarValue, 0);
    end;
     

    A better fix is to instead remove TScrollBox.WMMouseWheel entirely (if you're on Delphi 11) and put the code in a newly overridden DefaultHandler:

     

      TScrollBox = class(TScrollingWinControl)
      public
        ...
        procedure DefaultHandler(var Message); override;
      end;
    
    procedure TScrollBox.DefaultHandler(var Message);
    const
      LScrollBarValues: array[Boolean] of WPARAM = (SB_LINEDOWN, SB_LINEUP);
    var
      LScrollBarValue: WPARAM;
      LLineToScroll: Integer;
    begin
      if (WindowHandle <> 0) and (TMessage(Message).Msg = WM_MOUSEWHEEL) and
        VertScrollBar.IsScrollBarVisible then
      begin
        LScrollBarValue := LScrollBarValues[TWMMouseWheel(Message).WheelDelta > 0];
        for LLineToScroll := 1 to Mouse.WheelScrollLines do
          Perform(WM_VSCROLL, LScrollBarValue, 0);
        TMessage(Message).Result := 1;
        Exit;
      end;
      inherited;
    end;

    The advantage of using DefaultHandler instead of a WM_MOUSEWHEEL message handler is that the scrollbox now works just like native Win32 controls do, where the parents all the way up to the form get the OnMouseWheel messages, and the innermost scrollbox scrolls if none of them handle it.

     
    2) Patch VCL.Controls.pas  (fixes issue #4):
    function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean;
    var
      Control: TControl;
      P: TPoint;
      IsWheelMsg: Boolean;
    begin
      IsWheelMsg := (Message.Msg = WM_MOUSEWHEEL) or (Message.Msg = WM_MOUSEHWHEEL);
      if GetCapture = Handle then
      begin
        if (CaptureControl <> nil) and (CaptureControl.Parent = Self) then
          Control := CaptureControl
        else
          Control := nil;
      end
      else if IsWheelMsg then
        Control := ControlAtPos(ScreenToClient(SmallPointToPoint(Message.Pos)), False)
      else
        Control := ControlAtPos(SmallPointToPoint(Message.Pos), False);
      Result := False;
      if Control <> nil then
      begin
        if IsWheelMsg then
        begin
          Message.Result := Control.Perform(Message.Msg, Message.Keys, TMessage(Message).LParam);
          Result := (Message.Result <> 0);
        end
        else
        begin
          P.X := Message.XPos - Control.Left;
          P.Y := Message.YPos - Control.Top;
          Message.Result := Control.Perform(Message.Msg, Message.Keys, PointToLParam(P));
          Result := True;
        end;
      end;
    end;

    3) To make the mouse wheel consistently affect the control the mouse is over, regardless of which version of Windows you're running or which way the new setting is configured (which matches what official Microsoft apps do), add a TApplicationEvents object with this as the OnMessage handler. (Fixes issues #2 and #3)

     

    procedure TMyForm.ApplicationEventsMessage(var Msg: tagMSG;
      var Handled: Boolean);
    var
      Wnd: HWND;
    begin
      if (Msg.message = WM_MOUSEWHEEL) or (Msg.message = WM_MOUSEHWHEEL) then
      begin
        Wnd := GetCapture;
        if Wnd = 0 then
          Wnd := WindowFromPoint(SmallPointToPoint(TSmallPoint(Msg.lParam)));
        if Wnd <> 0 then
          msg.hwnd := Wnd;
      end;
    end;

    4) To fix the smeared appearance, you can

        A) Turn on VertScrollBar.Smooth, which triggers an Update call between each scroll

        B) Call Update yourself, either in the OnMouseWheel handler or in the TScrollBox WMMouseWheel/DefaultHandler method in between Perform calls.

        C) Change TScrollBox WMMouseWheel/DefaultHandler's Perform loop to the below, to update the scrollbar position once instead of with repeated WM_VSCROLL messages.

        if VertScrollBar.Smooth then
        begin
          LScrollBarValue := LScrollBarValues[TWMMouseWheel(Message).WheelDelta > 0];
          for LLineToScroll := 1 to Mouse.WheelScrollLines do
            Perform(WM_VSCROLL, LScrollBarValue, 0);
        end
        else
          { Move the scrollbar directly to only update the position once.
            TControlScrollBar.Increment uses different defaults for smooth vs chunky
            scrolling (TControlScrollBar.Update), so this may produce smaller scrolls
            than if you set Smooth:=true }
          VertScrollBar.Position := VertScrollBar.Position +
            VertScrollBar.Increment * Mouse.WheelScrollLines *
            -TWMMouseWheel(Message).WheelDelta div WHEEL_DELTA;

    And yes, none of the above should be necessary because the VCL should handle all of this.  The IsControlMouseMsg bugs have existed forever though and appear to have been reported as QC-135258 at least as far back as 2015.

    • Like 4

  5. This is "Does anyone know WTF is supposed to happen?", not "I want to do this and think it should work":


    The code below has two class helpers, one descending from the other.  The parent one has a virtual method and the descendent overrides it.  The compiler accepts it without any errors/warnings/hints.  Actually running prints

     

    Quote

    Bar

    BaseClass.Foo

    ---

    SubClass.Foo

     

    So the parent helper doesn't care about it being virtual, and the subclass ignores the 'inherited' call.

     

    If you don't actually create the object before calling Obj.Foo, it raises an access violation in  System.pas::_GetHelperIntf().  

     

    I found an old blog post from Joe White where he stumbled on it back in 2007, but back then _GetHelperIntf() was essentially a no-op, and it now has code.  Obviously since class helpers don't have their own VMTs, the code doesn't make sense, but why does the compiler accept it?  I don't see anything in the class and record helper help about declaring interfaces, and AFAIK they still don't support interface helpers.  Anyone have any idea what's going on here?

    program ClassHelperTest;
    
    {$APPTYPE CONSOLE}
    
    {$R *.res}
    
    uses
      System.SysUtils;
    
    type
      TObjHelper = class helper for TObject
    	procedure Foo; virtual;
    	procedure Bar;
      end;
    
      TSubObjHelper = class helper(TObjHelper) for TObject
    	procedure Foo; override;
      end;
    
    procedure TObjHelper.Bar;
    begin
      WriteLn('Bar');
      Foo;
    end;
    
    procedure TObjHelper.Foo;
    begin
      WriteLn('BaseClass.Foo');
    end;
    
    procedure TSubObjHelper.Foo;
    begin
      inherited;
      WriteLn('SubClass.Foo');
    end;
    
    var
      Obj: TObject;
    begin
      Obj := TObject.Create;
      Obj.Bar;
      WriteLn('---');
      Obj.Foo;
      Obj.Free;
    end.

     

    • Like 1

  6. 11 hours ago, Tommi Prami said:

    What is disadvantage and/or deal breaker not to use Attribute based library? Why it won't work for you? (Just genuinely curious) ?

    Like Uwe said, I'm trying to serialize objects I don't control (stock VCL components), so I can't modify the original declarations.

     

    Part of is learning curve too.  This started for personal use and I wasn't expecting to spend days researching JSON serializers.  I started by just passing the base object into various framework's ObjectToJSON routines.  Delphi's built-in ones fail with a marshalling error several layers deep, with no indication of what it was faulting on.  Neon, I think, failed because it decided that the object should be stored as an array since it had a "Count" property, and there wasn't a way to force it to do otherwise.  I looked at a few other frameworks too and don't remember exactly how they all came up short, but my starting point was "This is a TPersistent descendent that already has published properties; it should be easy to get at least a minimal export without doing anything fancy", and aside from mORMOT and TMS's FNC framework, that wasn't true.

     

    Other than that though, I largely agree with the arguments Dalija covered in her blog: Part 1Part 2Part 3Part 4


  7. 6 hours ago, SwiftExpat said:

    TMS FNC Core has a a library that works with published properties.  It is licensed with FNC. 

    TMS blog post describing it here:  https://biz.tmssoftware.com/site/blog.asp?post=646

    I did see that one in my search and should have looked at it more closely.  Ultimately I avoided it because I was trying to stick to an open source library to make it easier to share the code, and then sort of forgot about it. 🙂

    6 hours ago, SwiftExpat said:

    Here is your code forked / converted to work with FNC : https://github.com/SwiftExpat/vsf2json .  EXE and a json sample are in the releases so you can try it out if you like.

    Nice! StyleObjects is definitely a better name for that part than "Objects" is. 🙂  The FNC version is missing a bunch of the data I had to manually construct in the mORMot version though:  Colors, SysColors, and Fonts are public sub-objects of TSeStyleSource and none of them have properties that match the name/value pairs that they represent.  That's what the "WriteColors", "WriteSysColors" and "WriteFonts" functions were doing, which were loosely based on the SaveToStream functions in the respective objects (TSeStyleColors, TSeStyleSysColors, TSeStyleFonts).  The style objects also support nesting, and your version only includes the top-level of it.  


  8. 6 hours ago, David Schwartz said:

    Couldn't you derive a child class, redefine the properties to reflect those in the ancestor class, then add the necessary attributes to them?

    As Uwe said, that wouldn't have worked since the parent object was creating child objects of the appropriate type, and I don't have any control over that.  For context, it was for a VCL style to JSON converter and I want to use the VCL's existing style loading code.

     

    Regardless, I still think it's weird that literally every JSON serialization library I could find relied on attributes and public/private fields.  Delphi has had TPersistent and properties with 'stored' and 'default' and different names than the backing fields for its entire existence.  They work well.  It's great that the newer extended RTTI/attribute approach is available, but I absolutely do not understand why that's the only approach anyone's used.


  9. 5 hours ago, Dalija Prasnikar said:

    I am working on one, but it is work in progress. Intention is to make this during follow up series of blogposts. Since I am quite busy at the moment, I am not sure when will this see the light of day.

    Understandable, and thank you for the initial posts.  We have a settings framework internally that works that way and works great, but it uses XML and would have been hard to extract.  By the time I'd gotten to your posts I'd already run through half a dozen different JSON serialization libraries that all used extended RTTI and attributes, and I was beginning to think I was crazy for expecting one to take advantage of Delphi's long established persistence support.


  10. I just released a converter to go from Delphi VCL/FireMonkey style files (.vsf) to a structured JSON dump.  Embedded bitmaps are ignored, and it's one-way only, but it's handy for comparing changes.  There's a Beyond Compare 4 file format available as well, though if you have questions/issues, post here or on the Github page rather than going through Scooter Software's official support.  

     

    https://github.com/boramis/vsf2json

    • Like 1

  11. Are there any object-to-JSON serialization libraries available that work with published properties (not public, not fields) and which allow customizing the serialization externally to the class?  Dalija talked about this in her "When in Rome" series (https://dalijap.blogspot.com/2021/04/when-in-rome-do-as-romans-do-part-i.html) but didn't point to an alternative that worked that way.  Any license is fine, though open source of some sort would be preferable.

     

    Specifically, I'm working with third party TComponent classes which already have most of their state exposed as published properties, and I can't add attributes to customize things.  A couple of the components do use DefineProperties for some additional state when stored as DFMs, and I need to be able to register some sort of callback to serialize those.  I was able to get close with mORMot's ObjectToJSON with RegisterCustomSerializer, but it doesn't support mixing published and custom properties in a single class.  All of the other ones I've found do public/private fields, require attributes in the original classes, or fail in the marshalling step with unhelpful error messages.

     

    Edit: I figured a workaround using mORMot for my specific usage, so I don't need this anymore, though it is a bit hacky, so if there's a better approach, I certainly won't mind hearing it.

    • Thanks 1

  12. On 9/23/2020 at 3:34 AM, luebbe said:

    The joke is: There is a "SystemColors" checkbox, but obviously Emba decided not to call StyleServices.GetSystemColor(), so I had to turn it of and tried to adjust the titlebar colors manually.

    This works, sort of, but the windows border color is not the style's border color anymore. So this has to be set manually as well...

    Is there a way to set the form's border color when using TTitleBarPanel if you're not using seBorder in the StyleElements?  For our usage this would be workable short term, if I can tone down the bright white window frame


  13. Currently all of our forms are designed using Tahoma 8 at 96dpi and then scaled up at runtime based on the system font and DPI.  That's been working great, but there's a snag with the newer DPI-based scaling in XE7+: since it doesn't scale based on the font anymore, loading on Vista/Win7/Win10 where they use Segoe UI 9 makes things too cramped/clipped.  I could add a manual ChangeScale call at runtime to handle that, but since we're not targetting XP anymore, it would be nice to just scale all of our design-time forms once and move everything over to using Segoe UI all the time. Has anyone else run into this, and if so, any solutions besides hand adjusting hundreds of forms?  A "Scale form by X%" IDE expert, for example?

    • Like 1
×