1{%MainUnit ../extctrls.pas} 2{****************************************************************************** 3 TCustomRadioGroup 4 ****************************************************************************** 5 6 ***************************************************************************** 7 This file is part of the Lazarus Component Library (LCL) 8 9 See the file COPYING.modifiedLGPL.txt, included in this distribution, 10 for details about the license. 11 ***************************************************************************** 12 13 Delphi compatibility: 14 15 - the interface is almost like in delphi 5 16} 17 18 19type 20 21 { TRadioGroupStringList } 22 23 TRadioGroupStringList = class(TStringList) 24 private 25 FRadioGroup: TCustomRadioGroup; 26 protected 27 procedure Changed; override; 28 public 29 constructor Create(TheRadioGroup: TCustomRadioGroup); 30 procedure Assign(Source: TPersistent); override; 31 end; 32 33{ TRadioGroupStringList } 34 35procedure TRadioGroupStringList.Changed; 36begin 37 inherited Changed; 38 if (UpdateCount = 0) then 39 FRadioGroup.UpdateAll 40 else 41 FRadioGroup.UpdateInternalObjectList; 42 FRadioGroup.FLastClickedItemIndex := FRadioGroup.FItemIndex; 43end; 44 45constructor TRadioGroupStringList.Create(TheRadioGroup: TCustomRadioGroup); 46begin 47 inherited Create; 48 FRadioGroup := TheRadioGroup; 49end; 50 51procedure TRadioGroupStringList.Assign(Source: TPersistent); 52var 53 SavedIndex: Integer; 54begin 55 SavedIndex := FRadioGroup.ItemIndex; 56 inherited Assign(Source); 57 if SavedIndex < Count then FRadioGroup.ItemIndex := SavedIndex; 58end; 59 60 61{------------------------------------------------------------------------------ 62 Method: TCustomRadioGroup.Create 63 Params: TheOwner: the owner of the class 64 Returns: Nothing 65 66 Constructor for the radiogroup 67 ------------------------------------------------------------------------------} 68constructor TCustomRadioGroup.Create(TheOwner : TComponent); 69begin 70 inherited Create (TheOwner); 71 ControlStyle := ControlStyle + [csCaptureMouse, csClickEvents, csSetCaption, 72 csDoubleClicks]; 73 FItems := TRadioGroupStringList.Create(Self); 74 FAutoFill := true; 75 FItemIndex := -1; 76 FLastClickedItemIndex := -1; 77 FButtonList := TFPList.Create; 78 FColumns := 1; 79 FColumnLayout := clHorizontalThenVertical; 80 ChildSizing.Layout:=cclLeftToRightThenTopToBottom; 81 ChildSizing.ControlsPerLine:=FColumns; 82 ChildSizing.ShrinkHorizontal:=crsScaleChilds; 83 ChildSizing.ShrinkVertical:=crsScaleChilds; 84 ChildSizing.EnlargeHorizontal:=crsHomogenousChildResize; 85 ChildSizing.EnlargeVertical:=crsHomogenousChildResize; 86 ChildSizing.LeftRightSpacing:=6; 87 ChildSizing.TopBottomSpacing:=0; 88end; 89 90 91{------------------------------------------------------------------------------ 92 Method: TCustomRadioGroup.Destroy 93 Params: none 94 Returns: Nothing 95 96 Destructor for the radiogroup 97 ------------------------------------------------------------------------------} 98destructor TCustomRadioGroup.Destroy; 99begin 100 FreeAndNil(FItems); 101 FreeAndNil(FButtonList); 102 FreeAndNil(FHiddenButton); 103 inherited Destroy; 104end; 105 106{------------------------------------------------------------------------------ 107 Method: TCustomRadioGroup.InitializeWnd 108 Params: none 109 Returns: Nothing 110 111 Create the visual component of the Radiogroup. 112 ------------------------------------------------------------------------------} 113procedure TCustomRadioGroup.InitializeWnd; 114 115 procedure RealizeItemIndex; 116 var 117 i: Integer; 118 begin 119 if (FItemIndex <> -1) and (FItemIndex<FButtonList.Count) then 120 TRadioButton(FButtonList[FItemIndex]).Checked := true 121 else if FHiddenButton<>nil then 122 FHiddenButton.Checked:=true; 123 for i:=0 to FItems.Count-1 do begin 124 TRadioButton(FButtonList[i]).Checked := fItemIndex = i; 125 end; 126 end; 127 128begin 129 if FCreatingWnd then RaiseGDBException('TCustomRadioGroup.InitializeWnd'); 130 FCreatingWnd := true; 131 //DebugLn(['[TCustomRadioGroup.InitializeWnd] A ',DbgSName(Self),' FItems.Count=',FItems.Count,' HandleAllocated=',HandleAllocated,' ItemIndex=',ItemIndex]); 132 UpdateItems; 133 inherited InitializeWnd; 134 RealizeItemIndex; 135 //debugln(['TCustomRadioGroup.InitializeWnd END']); 136 FCreatingWnd := false; 137end; 138 139function TCustomRadioGroup.Rows: integer; 140begin 141 if FItems.Count>0 then 142 Result:=((FItems.Count-1) div Columns)+1 143 else 144 Result:=0; 145end; 146 147procedure TCustomRadioGroup.ItemEnter(Sender: TObject); 148begin 149 DoEnter; 150end; 151 152procedure TCustomRadioGroup.ItemExit(Sender: TObject); 153begin 154 DoExit; 155end; 156 157procedure TCustomRadioGroup.ItemResize(Sender: TObject); 158begin 159 160end; 161 162procedure TCustomRadioGroup.UpdateItems; 163var 164 i: integer; 165 ARadioButton: TRadioButton; 166begin 167 if FUpdatingItems then exit; 168 FUpdatingItems:=true; 169 try 170 // destroy radiobuttons, if there are too many 171 while FButtonList.Count>FItems.Count do 172 begin 173 TRadioButton(FButtonList[FButtonList.Count-1]).Free; 174 FButtonList.Delete(FButtonList.Count-1); 175 end; 176 177 // create as many TRadioButton as needed 178 while (FButtonList.Count<FItems.Count) do 179 begin 180 ARadioButton := TRadioButton.Create(Self); 181 with ARadioButton do 182 begin 183 Name := 'RadioButton'+IntToStr(FButtonList.Count); 184 OnClick := @Self.Clicked; 185 OnChange := @Self.Changed; 186 OnEnter := @Self.ItemEnter; 187 OnExit := @Self.ItemExit; 188 OnKeyDown := @Self.ItemKeyDown; 189 OnKeyUp := @Self.ItemKeyUp; 190 OnKeyPress := @Self.ItemKeyPress; 191 OnUTF8KeyPress := @Self.ItemUTF8KeyPress; 192 OnResize := @Self.ItemResize; 193 ParentFont := True; 194 BorderSpacing.CellAlignHorizontal := ccaLeftTop; 195 BorderSpacing.CellAlignVertical := ccaCenter; 196 ControlStyle := ControlStyle + [csNoDesignSelectable]; 197 end; 198 FButtonList.Add(ARadioButton); 199 end; 200 if FHiddenButton=nil then begin 201 FHiddenButton:=TRadioButton.Create(nil); 202 with FHiddenButton do 203 begin 204 Name := 'HiddenRadioButton'; 205 Visible := False; 206 ControlStyle := ControlStyle + [csNoDesignSelectable, csNoDesignVisible]; 207 end; 208 end; 209 210 if (FItemIndex>=FItems.Count) and not (csLoading in ComponentState) then FItemIndex:=FItems.Count-1; 211 212 if FItems.Count>0 then 213 begin 214 // to reduce overhead do it in several steps 215 216 // assign Caption and then Parent 217 for i:=0 to FItems.Count-1 do 218 begin 219 ARadioButton := TRadioButton(FButtonList[i]); 220 ARadioButton.Caption := FItems[i]; 221 ARadioButton.Parent := Self; 222 end; 223 FHiddenButton.Parent:=Self; 224 225 // the checked and unchecked states can be applied only after all other 226 for i := 0 to FItems.Count-1 do 227 begin 228 ARadioButton := TRadioButton(FButtonList[i]); 229 ARadioButton.Checked := (i = FItemIndex); 230 ARadioButton.Visible := true; 231 end; 232 //FHiddenButton must remain the last item in Controls[], so that Controls[] is in sync with Items[] 233 Self.RemoveControl(FHiddenButton); 234 Self.InsertControl(FHiddenButton); 235 if HandleAllocated then 236 FHiddenButton.HandleNeeded; 237 FHiddenButton.Checked := (FItemIndex = -1); 238 UpdateTabStops; 239 end; 240 finally 241 FUpdatingItems:=false; 242 end; 243end; 244 245procedure TCustomRadioGroup.UpdateControlsPerLine; 246var 247 NewControlsPerLine: LongInt; 248begin 249 if ChildSizing.Layout=cclLeftToRightThenTopToBottom then 250 NewControlsPerLine:=Max(1,FColumns) 251 else 252 NewControlsPerLine:=Max(1,Rows); 253 ChildSizing.ControlsPerLine:=NewControlsPerLine; 254 //DebugLn('TCustomRadioGroup.UpdateControlsPerLine ',dbgs(ChildSizing.ControlsPerLine),' ',dbgs(NewControlsPerLine),' FColumns=',dbgs(FColumns),' FItems.Count=',dbgs(FItems.Count),' ',dbgs(ChildSizing.Layout=cclLeftToRightThenTopToBottom)); 255end; 256 257procedure TCustomRadioGroup.ItemKeyDown(Sender: TObject; var Key: Word; 258 Shift: TShiftState); 259 260 procedure MoveSelection(HorzDiff, VertDiff: integer); 261 var 262 Count: integer; 263 StepSize: integer; 264 BlockSize : integer; 265 NewIndex : integer; 266 WrapOffset: integer; 267 begin 268 Count := FButtonList.Count; 269 if FColumnLayout=clHorizontalThenVertical then begin 270 //add a row for ease wrapping 271 BlockSize := Columns * (Rows+1); 272 StepSize := HorzDiff + VertDiff * Columns; 273 WrapOffSet := VertDiff; 274 end 275 else begin 276 //add a column for ease wrapping 277 BlockSize := (Columns+1) * Rows; 278 StepSize := HorzDiff * Rows + VertDiff; 279 WrapOffSet := HorzDiff; 280 end; 281 NewIndex := ItemIndex; 282 repeat 283 Inc(NewIndex, StepSize); 284 if (NewIndex >= Count) or (NewIndex < 0) then begin 285 NewIndex := (NewIndex + WrapOffSet + BlockSize) mod BlockSize; 286 // Keep moving in the same direction until in valid range 287 while NewIndex >= Count do 288 NewIndex := (NewIndex + StepSize) mod BlockSize; 289 end; 290 until (NewIndex = ItemIndex) or TRadioButton(FButtonList[NewIndex]).Enabled; 291 ItemIndex := NewIndex; 292 TRadioButton(FButtonList[ItemIndex]).SetFocus; 293 Key := 0; 294 end; 295 296begin 297 if Shift=[] then begin 298 case Key of 299 VK_LEFT: MoveSelection(-1,0); 300 VK_RIGHT: MoveSelection(1,0); 301 VK_UP: MoveSelection(0,-1); 302 VK_DOWN: MoveSelection(0,1); 303 end; 304 end; 305 if Key <> 0 then 306 KeyDown(Key, Shift); 307end; 308 309procedure TCustomRadioGroup.ItemKeyUp(Sender: TObject; var Key: Word; 310 Shift: TShiftState); 311begin 312 if Key <> 0 then 313 KeyUp(Key, Shift); 314end; 315 316procedure TCustomRadioGroup.ItemKeyPress(Sender: TObject; var Key: Char); 317begin 318 if Key <> #0 then 319 KeyPress(Key); 320end; 321 322procedure TCustomRadioGroup.ItemUTF8KeyPress(Sender: TObject; 323 var UTF8Key: TUTF8Char); 324begin 325 UTF8KeyPress(UTF8Key); 326end; 327 328{------------------------------------------------------------------------------ 329 Method: TCustomRadioGroup.SetColumns 330 Params: value - no of columns of the radiogroup 331 Returns: Nothing 332 333 Set the FColumns property which determines the number of columns in 334 which the radiobuttons should be arranged. 335 Range: 1 .. ??? 336 ------------------------------------------------------------------------------} 337procedure TCustomRadioGroup.SetColumns(Value: integer); 338begin 339 if Value <> FColumns then begin 340 if (Value < 1) 341 then raise Exception.Create('TCustomRadioGroup: Columns must be >= 1'); 342 FColumns := Value; 343 UpdateControlsPerLine; 344 end; 345end; 346 347{------------------------------------------------------------------------------ 348 Method: TCustomRadioGroup.SetItems 349 Params: value - Stringlist containing items to be displayed as radiobuttons 350 Returns: Nothing 351 352 Assign items from a stringlist. 353 ------------------------------------------------------------------------------} 354procedure TCustomRadioGroup.SetItems(Value: TStrings); 355begin 356 if (Value <> FItems) then 357 begin 358 FItems.Assign(Value); 359 UpdateItems; 360 UpdateControlsPerLine; 361 end; 362end; 363 364{------------------------------------------------------------------------------ 365 Method: TCustomRadioGroup.SetItemIndex 366 Params: value - index of RadioButton to be selected 367 Returns: Nothing 368 369 Select one of the radiobuttons 370 ------------------------------------------------------------------------------} 371procedure TCustomRadioGroup.SetItemIndex(Value : integer); 372var 373 OldItemIndex: LongInt; 374 OldIgnoreClicks: Boolean; 375begin 376 //DebugLn('TCustomRadioGroup.SetItemIndex ',dbgsName(Self),' Old=',dbgs(FItemIndex),' New=',dbgs(Value)); 377 if Value = FItemIndex then exit; 378 // needed later if handle isn't allocated 379 OldItemIndex := FItemIndex; 380 if FReading then 381 FItemIndex:=Value 382 else begin 383 if (Value < -1) or (Value >= FItems.Count) then 384 raise Exception.CreateFmt(rsIndexOutOfBounds,[ClassName,Value,FItems.Count-1]); 385 386 if (HandleAllocated) then 387 begin 388 // the radiobuttons are grouped by the widget interface 389 // and some does not allow to uncheck all buttons in a group 390 // Therefore there is a hidden button 391 FItemIndex:=Value; 392 OldIgnoreClicks:=FIgnoreClicks; 393 FIgnoreClicks:=true; 394 try 395 if (FItemIndex <> -1) then 396 TRadioButton(FButtonList[FItemIndex]).Checked := true 397 else 398 FHiddenButton.Checked:=true; 399 // uncheck old radiobutton 400 if (OldItemIndex <> -1) then begin 401 if (OldItemIndex>=0) and (OldItemIndex<FButtonList.Count) then 402 TRadioButton(FButtonList[OldItemIndex]).Checked := false 403 end else 404 FHiddenButton.Checked:=false; 405 finally 406 FIgnoreClicks:=OldIgnoreClicks; 407 end; 408 // this has automatically unset the old button. But they do not recognize 409 // it. Update the states. 410 CheckItemIndexChanged; 411 UpdateTabStops; 412 413 OwnerFormDesignerModified(Self); 414 end else 415 begin 416 FItemIndex := Value; 417 // maybe handle was recreated. issue #26714 418 FLastClickedItemIndex := -1; 419 420 // trigger event to be delphi compat, even if handle isn't allocated. 421 // issue #15989 422 if (Value <> OldItemIndex) and not FCreatingWnd then 423 begin 424 if Assigned(FOnClick) then FOnClick(Self); 425 if Assigned(FOnSelectionChanged) then FOnSelectionChanged(Self); 426 FLastClickedItemIndex := FItemIndex; 427 end; 428 end; 429 end; 430 //DebugLn('TCustomRadioGroup.SetItemIndex ',dbgsName(Self),' END Old=',dbgs(FItemIndex),' New=',dbgs(Value)); 431end; 432 433{------------------------------------------------------------------------------ 434 Method: TCustomRadioGroup.GetItemIndex 435 Params: value - index of RadioButton to be selected 436 Returns: Nothing 437 438 Retrieve the index of the radiobutton currently selected. 439 ------------------------------------------------------------------------------} 440function TCustomRadioGroup.GetItemIndex : integer; 441begin 442 //debugln('TCustomRadioGroup.GetItemIndex ',dbgsName(Self),' FItemIndex=',dbgs(FItemIndex)); 443 Result := FItemIndex; 444end; 445 446procedure TCustomRadioGroup.CheckItemIndexChanged; 447begin 448 if FCreatingWnd or FUpdatingItems then 449 exit; 450 if [csLoading,csDestroying]*ComponentState<>[] then exit; 451 UpdateRadioButtonStates; 452 if [csDesigning]*ComponentState<>[] then exit; 453 if FLastClickedItemIndex=FItemIndex then exit; 454 FLastClickedItemIndex:=FItemIndex; 455 EditingDone; 456 // for Delphi compatibility: OnClick should be invoked, whenever ItemIndex 457 // has changed 458 if Assigned (FOnClick) then FOnClick(Self); 459 // And a better named LCL equivalent 460 if Assigned (FOnSelectionChanged) then FOnSelectionChanged(Self); 461end; 462 463{------------------------------------------------------------------------------ 464 Method: TCustomRadioGroup.CanModify 465 Params: none 466 Returns: always true 467 468 Is the user allowed to select a different radiobutton? 469 ------------------------------------------------------------------------------} 470function TCustomRadioGroup.CanModify : boolean; 471begin 472 Result := true; 473end; 474 475{------------------------------------------------------------------------------ 476 Method: TCustomRadioGroup.ReadState 477 Params: Reader: TReader 478 479 executed when component is read from stream 480 ------------------------------------------------------------------------------} 481procedure TCustomRadioGroup.ReadState(Reader: TReader); 482begin 483 FReading := True; 484 inherited ReadState(Reader); 485 FReading := False; 486 if (fItemIndex<-1) or (fItemIndex>=FItems.Count) then fItemIndex:=-1; 487 FLastClickedItemIndex:=FItemIndex; 488end; 489 490{------------------------------------------------------------------------------ 491 Method: TCustomRadioGroup.Clicked 492 Params: sender - the calling object 493 494 This is the callback for all radiobuttons in the group. If an OnClick 495 handler is assigned it will be called 496 ------------------------------------------------------------------------------} 497procedure TCustomRadioGroup.Clicked(Sender : TObject); 498Begin 499 if FIgnoreClicks then exit; 500 CheckItemIndexChanged; 501end; 502 503{------------------------------------------------------------------------------ 504 Method: TCustomRadioGroup.Changed 505 Params: sender - the calling object 506 507 Checks for changes. Does the same as Clicked for Delphi compatibility. 508 ------------------------------------------------------------------------------} 509procedure TCustomRadioGroup.Changed(Sender : TObject); 510Begin 511 CheckItemIndexChanged; 512end; 513 514procedure TCustomRadioGroup.UpdateTabStops; 515var 516 i: Integer; 517 RadioBtn: TRadioButton; 518begin 519 for i := 0 to FButtonList.Count - 1 do 520 begin 521 RadioBtn := TRadioButton(FButtonList[i]); 522 RadioBtn.TabStop := RadioBtn.Checked; 523 end; 524end; 525 526class procedure TCustomRadioGroup.WSRegisterClass; 527begin 528 inherited WSRegisterClass; 529 RegisterCustomRadioGroup; 530end; 531 532procedure TCustomRadioGroup.UpdateInternalObjectList; 533begin 534 UpdateItems; 535end; 536 537procedure TCustomRadioGroup.UpdateAll; 538begin 539 UpdateItems; 540 UpdateControlsPerLine; 541 OwnerFormDesignerModified(Self); 542end; 543 544procedure TCustomRadioGroup.SetAutoFill(const AValue: Boolean); 545begin 546 if FAutoFill=AValue then exit; 547 FAutoFill:=AValue; 548 DisableAlign; 549 try 550 if FAutoFill then begin 551 ChildSizing.EnlargeHorizontal:=crsHomogenousChildResize; 552 ChildSizing.EnlargeVertical:=crsHomogenousChildResize; 553 end else begin 554 ChildSizing.EnlargeHorizontal:=crsAnchorAligning; 555 ChildSizing.EnlargeVertical:=crsAnchorAligning; 556 end; 557 finally 558 EnableAlign; 559 end; 560end; 561 562procedure TCustomRadioGroup.SetColumnLayout(const AValue: TColumnLayout); 563begin 564 if FColumnLayout=AValue then exit; 565 FColumnLayout:=AValue; 566 if FColumnLayout=clHorizontalThenVertical then 567 ChildSizing.Layout:=cclLeftToRightThenTopToBottom 568 else 569 ChildSizing.Layout:=cclTopToBottomThenLeftToRight; 570 UpdateControlsPerLine; 571end; 572 573procedure TCustomRadioGroup.FlipChildren(AllLevels: Boolean); 574begin 575 // no flipping 576end; 577 578{------------------------------------------------------------------------------ 579 procedure TCustomRadioGroup.UpdateRadioButtonStates; 580 581 Read all Checked properties of all radiobuttons, to update any changes in 582 the interface to the LCL. 583 ------------------------------------------------------------------------------} 584procedure TCustomRadioGroup.UpdateRadioButtonStates; 585var 586 i: Integer; 587begin 588 FItemIndex:=-1; 589 FHiddenButton.Checked; 590 for i:=0 to FButtonList.Count-1 do 591 if TRadioButton(FButtonList[i]).Checked then FItemIndex:=i; 592 UpdateTabStops; 593end; 594