View Issue Details

IDProjectCategoryView StatusLast Update
0033313LazarusLCLpublic2018-03-04 09:51
ReporterRolf Wetjen Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
PlatformWindowsOSWindows 10 x64 
Product Version1.9 (SVN) 
Summary0033313: Strange ComboBox behavior (Windows)
DescriptionThere are two issues with a ComboBox (csDropDown) control in Windows:

1. ComboBox.AutoSelect:=false isn't working. I seems that Windows always selects the whole ComboBox.Text at activation of the control.

2. Changing the ComboBox.Font (.Style, .Size or .Color) overwrites the ComboBox.Text with the first matching item of ComboBox.Items.
Look at this code:

procedure TForm1.SpeedButtonTestClick (Sender: TObject);
var
  fs: TFontStyles;
begin
  ComboBox1.Clear;
  ComboBox1.AddItem('Item0',nil);
  ComboBox1.AddItem('TestItem1',nil);
  ComboBox1.AddItem('Item2',nil);
  ComboBox1.Text:='Test';

  fs:=ComboBox1.Font.Style;
  if fsBold in fs then
    fs:=fs-[fsBold]
  else
    fs:=fs+[fsBold];
  ComboBox1.Font.Style:=fs; // Changes ComboBox1.Text to 'TestItem1'
end;

ComboBox1.Text is set to 'TestItem1'.

I'm quite sure that this is a Windows issue and doesn't occur with Linux althought I can't test this.
Additional InformationI've attached a demo and a patch for both issues for win32wsstdctrls.pp.
The ComboBox will behave in the same way as an Edit control for AutoSelect=false (Mouse and Keyboard activation).
The ComboBox.Text (and the cursor position) will not be changed at changes to the font.
TagsNo tags attached.
Fixed in Revision
LazTarget
WidgetsetWin32/Win64
Attached Files

Activities

Rolf Wetjen

2018-03-04 09:51

reporter  

win32wsstdctrls.pp.patch (11,210 bytes)   
Index: win32wsstdctrls.pp
===================================================================
--- win32wsstdctrls.pp	(revision 57415)
+++ win32wsstdctrls.pp	(working copy)
@@ -428,6 +428,305 @@
   Result := WindowProc(Window, Msg, WParam, LParam);
 end;
 
+{-------------------------------------------------------------------------------
+This patch solves to ComboBox control issues:
+- The MS Windows ComboBox control doesn't support AutoSelect=false: It always
+  selects the whole text at activation.
+
+  With this patch the ComboBox (Style=csDropDown only) behaves like a
+  MS Windows Edit control at activation.
+  Mouse activation: The selection follows the mouse cursor as long as the left
+  button is pressed.
+  Key activation: The last selection is restored.
+
+- The MS Windows ComboBox (Style=csDropDown only) control replaces the
+  ComboBox.Text with the first matching item of the items list at any change of
+  the ComboBox font if ComboBox.Text is a prefix of one of the strings in the
+  item list.
+
+  This patch perserves the text and selection.
+
+
+  Relevant message flow during activation:
+
+  WM_LBUTTONDOWN (Mouse activation only)
+    Set saved selection to (FMouseSelect,FMouseSelect)
+
+  WM_SETFOCUS
+    Set FActivation for the next EM_SETSEL action.
+  EM_SETSEL
+    Restore the saved selection.
+    Release FActivation.
+
+  WM_MOUSEMOVE (Mouse activation only)
+    Set selection to (FMouseSelect,mouse position).
+
+  WM_KILLFOCUS
+    Save selection.
+    Unselect everything to hide the selection.
+
+  Relevant message flow during font changes:
+  WM_SETFONT
+    Save selection.
+    Set FFontChange for the next WM_SETTEXT and WM_SETSEL actions.
+  WM_SETTEXT
+    Suppress this action.
+  EM_SETSEL
+    Suppress this action.
+    Release FFontChange.
+    Restore selection.
+-------------------------------------------------------------------------------}
+
+type
+  ERWxComboBox = class(Exception);
+  TASCFPatchData = record
+    FItemHandle:         THandle;      // Handle of the edit control part of the ComboBox
+    FComboBox:           TCustomComboBox;
+    // AutoSelect=false patch
+    FActivation:         boolean;      // Set during activation
+    FAutoSelectSelStart: integer;      // Save selection on WM_KILLFOCUS
+    FAutoSelectSelEnd:   integer;      // Save selection on WM_KILLFOCUS
+    FMouseSelect:        integer;      // -1 or index of the character at mouse position during activation
+    // Font change patch
+    FFontChange:         boolean;      // Set during font change
+  end;
+  PASCFPatchData = ^TASCFPatchData;
+
+function ComboBoxItemSubClass (hWnd: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM;
+                       {%H-}uISubClass: UINT_PTR; dwRefData: DWORD_PTR):LRESULT; stdcall;
+var
+  PPD:   PASCFPatchData;
+  ss,se: DWORD;
+  pt:    TPoint;
+begin
+  PPD:={%H-}PASCFPatchData(dwRefData);
+
+  if (dwRefData=0) or
+     (PPD^.FItemHandle=INVALID_HANDLE_VALUE) then
+  begin
+    Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+    exit;
+  end;
+
+  if hWnd<>PPD^.FItemHandle then
+  begin
+    Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+    exit;
+  end;
+
+  case uMsg of
+    WM_LBUTTONDOWN:
+    // Patch AutoSelect=false
+    begin
+      if PPD^.FComboBox.AutoSelect then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      PPD^.FMouseSelect:=-1;
+      if GetFocus=hWnd then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      // Set selection acording to mouse position.
+      // Save cursor position to FMouseSelect.
+      if not GetCursorPos(pt) then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      if not Windows.ScreenToClient(hWnd,pt) then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      ss:=LOWORD(SendMessage(hWnd,EM_CHARFROMPOS,0,MAKELPARAM(pt.x,pt.y)));
+      if ss>=$FFFF then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      PPD^.FMouseSelect:=ss;
+      // Change saved selection for use with EM_SETSEL later
+      PPD^.FAutoSelectSelStart:=ss;
+      PPD^.FAutoSelectSelEnd:=ss;
+      Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+      exit;
+    end;
+
+    WM_SETFOCUS:
+    // Patch AutoSelect=false
+    begin
+      if PPD^.FComboBox.AutoSelect then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      // Supress ComboBox control selection setting
+      PPD^.FActivation:=true;
+      Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+      exit;
+    end;
+
+    WM_MOUSEMOVE:
+    // Patch AutoSelect=false
+    // Set current selection acording to mouse position.
+    begin
+      if PPD^.FComboBox.AutoSelect then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+      if ((wParam and MK_LBUTTON)=0) or
+         (PPD^.FMouseSelect<0) then
+        exit;
+      pt.x:=LOWORD(lParam);
+      pt.y:=HIWORD(lParam);
+      se:=LOWORD(SendMessage(hWnd,EM_CHARFROMPOS,0,MAKELPARAM(pt.x,pt.y)));
+      if se>=$FFFF then
+        exit;
+      // Set selection to mouse position. Override the patch.
+      if PPD^.FMouseSelect<>se then
+        DefSubClassProc(hWnd,EM_SETSEL,PPD^.FMouseSelect,se);
+      exit;
+    end;
+
+    WM_KILLFOCUS:
+    // Patch AutoSelect=false
+    begin
+      if PPD^.FComboBox.AutoSelect then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      // Get and save the current selection.
+      // Ensure to get the "real" (unpatched) values.
+      // Unselect all to hide the selection.
+      DefSubClassProc(hWnd,EM_GETSEL,{%H-}Windows.WPARAM(@ss),{%H-}Windows.LPARAM(@se));
+      Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+      DefSubClassProc(hWnd,EM_SETSEL,-1,0);
+      PPD^.FAutoSelectSelStart:=ss;
+      PPD^.FAutoSelectSelEnd:=se;
+      exit;
+    end;
+
+    WM_SETFONT:
+    // Patch change font
+    begin
+      // Get the "real" (unpatched) current selection.
+      DefSubClassProc(hWnd,EM_GETSEL,{%H-}Windows.WPARAM(@ss),{%H-}Windows.LPARAM(@se));
+      // Supress ComboBox control WM_SETTEXT
+      PPD^.FFontChange:=true;
+      Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+      exit;
+    end;
+
+    WM_SETTEXT:
+    // Patch change font
+    // Skip if FSuppress_WM_SETTEXT
+    begin
+      if PPD^.FFontChange then
+        Result:=1
+      else
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+      exit;
+    end;
+
+    EM_GETSEL:
+    // Patch AutoSelect=false
+    begin
+      if PPD^.FComboBox.AutoSelect then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      if GetFocus=hWnd then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      // No focus: use the saved values
+      if (PPD^.FAutoSelectSelStart>=0) and
+         (PPD^.FAutoSelectSelEnd>=0) then
+      begin
+        Result:=1;
+        if wParam<>0 then
+          PDWORD(wParam)^:=PPD^.FAutoSelectSelStart;
+        if lParam<>0 then
+          PDWORD(lParam)^:=PPD^.FAutoSelectSelEnd;
+        exit;
+      end;
+      Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+      exit;
+    end;
+
+    EM_SETSEL:
+    begin
+      // Patch change font
+      // Don't change the cursor position
+      if PPD^.FFontChange then
+      begin
+        // Get current selection.
+        SendMessage(hWnd,EM_GETSEL,{%H-}Windows.WPARAM(@ss),{%H-}Windows.LPARAM(@se));
+        PPD^.FFontChange:=false;
+        // Skip this selection and restore the saved one. Don't use SendMessage or DefSubClassProc.
+        // Don't restore if not focused.
+        if GetFocus=HWnd then
+        begin
+          PostMessage(hWnd,EM_SETSEL,ss,se);
+        end;
+        Result:=1;
+        exit;
+      end;
+
+      // Patch AutoSelect=false
+      if PPD^.FComboBox.AutoSelect then
+      begin
+        Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+        exit;
+      end;
+      if PPD^.FActivation then
+      begin
+        if (PPD^.FAutoSelectSelStart<0) or
+           (PPD^.FAutoSelectSelEnd<0) then
+        begin
+          PPD^.FAutoSelectSelStart:=0;
+          PPD^.FAutoSelectSelEnd:=0;
+        end;
+        // Restore the saved selection
+        Result:=DefSubClassProc(hWnd,uMsg,PPD^.FAutoSelectSelStart,PPD^.FAutoSelectSelEnd);
+        PPD^.FActivation:=false;
+        PPD^.FAutoSelectSelStart:=-1;
+        PPD^.FAutoSelectSelEnd:=-1;
+        exit;
+      end;
+      Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+      if GetFocus<>hWnd then
+      begin                            // Not focused: Update saved selection
+        PPD^.FAutoSelectSelStart:=wParam;
+        PPD^.FAutoSelectSelEnd:=lParam;
+      end;
+    end;
+  end;   { case uMsg of }
+  Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+end;
+
+function ComboBoxSubclass (hWnd: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM;
+                           {%H-}uISubClass: UINT_PTR; dwRefData: DWORD_PTR):LRESULT; stdcall;
+begin
+  Result:=DefSubClassProc(hWnd,uMsg,wParam,lParam);
+  if uMsg=WM_NCDESTROY then
+  begin
+    if dwRefData<>0 then
+      Dispose({%H-}PASCFPatchData(dwRefData));
+    exit;
+  end;
+end;
+{--End of patch ---------------------------------------------------------------}
+
+
 function ScrollBarWindowProc(Window: HWnd; Msg: UInt; WParam: Windows.WParam;
     LParam: Windows.LParam): LResult; stdcall;
 var
@@ -947,6 +1246,7 @@
 var
   Params: TCreateWindowExParams;
   Info: TComboboxInfo;
+  PPD: PASCFPatchData;
 begin
   // general initialization of Params
   PrepareCreateWindow(AWinControl, AParams, Params);
@@ -979,6 +1279,26 @@
       WindowCreateInitBuddy(AWinControl, Params);
       BuddyWindowInfo^.isChildEdit := true;
       BuddyWindowInfo^.isComboEdit := true;
+
+      if not (csDesigning in AWinControl.ComponentState) then
+      begin
+        New(PPD);
+        PPD^.FItemHandle:=INVALID_HANDLE_VALUE;
+        PPD^.FComboBox:=TCustomComboBox(AWinControl);
+        PPD^.FActivation:=false;
+        PPD^.FAutoSelectSelStart:=-1;
+        PPD^.FAutoSelectSelEnd:=-1;
+        PPD^.FMouseSelect:=-1;
+        PPD^.FFontChange:=false;
+        PPD^.FItemHandle:=Info.hwndItem;
+        // Subclassing the ComboBox item control (Info.hwndItem).
+        // Do it always regardless of AutoSelect setting. AutoSelect may change at runtime.
+        if not SetWindowSubclass(Info.hwndItem,@ComboBoxItemSubClass,0,{%H-}DWORD_PTR(PPD)) then
+          raise ERWxComboBox.CreateFmt('SetWindowSubclass (item): %s',[SysErrorMessage(GetLastError)]);
+        // Subclassing the ComboBox control.
+        if not SetWindowSubclass(AWincontrol.Handle,@ComboBoxSubClass,0,{%H-}DWORD_PTR(PPD)) then
+          raise ERWxComboBox.CreateFmt('SetWindowSubclass: %s',[SysErrorMessage(GetLastError)]);
+      end;
     end
     else
       BuddyWindowInfo:=nil;
win32wsstdctrls.pp.patch (11,210 bytes)   

Issue History

Date Modified Username Field Change
2018-03-04 09:51 Rolf Wetjen New Issue
2018-03-04 09:51 Rolf Wetjen File Added: win32wsstdctrls.pp.patch