Combobox Style 'csDropDownList' in Delphi - delphi

I have created one form in delphi 7 and added one combobox on it. The combobox contains the list of items. I dont want that user can enter the value to Combobox so i have set
combobox.style := csDropDownList;
But thorugh code i want to use combobox.text := 'New Item'; but its not working. Note that the text I want to show is not in the list of items and I don't want to add it there. Please is any solution to this?

No, this is simply not the way the Windows combobox control works.
However, if you insist, and you don't care that your users will get confused, you can set Style to csDropDown and then do
procedure TForm1.ComboBox1KeyPress(Sender: TObject; var Key: Char);
begin
Key := #0;
end;
as the combobox' OnKeyPress event. Then the user cannot enter text manually, but can only choose from the items in the list. However, you can still set the text to anything you like (even if it isn't in the list) by setting the Text property:
ComboBox1.Text := 'Sample';

Set the ItemIndex property. You can get ComboBox.Items.IndexOf('New Item') to get the index of that text, if you don't already know it.
Combobox.ItemIndex := Combobox.Items.IndexOf('New item');

Below sample code demonstrates how you can draw custom text in response to a WM_DRAWITEM message sent to the ComboBox control's parent window (this should be the form for the sample to work, otherwise subclassing controls or full drawing of items of the control would be necessary).
To receive this message set the Style property of the control to 'csOwnerDrawFixed', but do not put a handler for the OnDrawItem event so that default drawing should be applied in all other cases that we intervene drawing.
The sample sets a text when ItemIndex is -1, but it can be adapted/tweaked otherwise. Note that the drawing code is not complete or accurate, the sample just demonstrates a way how it can be done:
type
TForm1 = class(TForm)
ComboBox1: TComboBox;
[..]
private
procedure WMDrawItem(var Msg: TWMDrawItem); message WM_DRAWITEM;
end;
[...]
procedure TForm1.WMDrawItem(var Msg: TWMDrawItem);
var
Font: HFONT;
begin
inherited;
if (Msg.Ctl = ComboBox1.Handle) and (Msg.DrawItemStruct.itemID = $FFFFFFFF) and
((Msg.DrawItemStruct.itemAction and ODA_DRAWENTIRE) = ODA_DRAWENTIRE) then begin
Font := SelectObject(Msg.DrawItemStruct.hDC, ComboBox1.Canvas.Font.Handle);
SelectObject(Msg.DrawItemStruct.hDC, GetStockObject(DC_BRUSH));
if (Msg.DrawItemStruct.itemState and ODS_SELECTED) = ODS_SELECTED then begin
SetDCBrushColor(Msg.DrawItemStruct.hDC, ColorToRGB(clHighlight));
SetBkColor(Msg.DrawItemStruct.hDC, ColorToRGB(clHighlight));
SetTextColor(Msg.DrawItemStruct.hDC, ColorToRGB(clHighlightText));
end else begin
SetDCBrushColor(Msg.DrawItemStruct.hDC, ColorToRGB(clWindow));
SetBkColor(Msg.DrawItemStruct.hDC, ColorToRGB(clWindow));
SetTextColor(Msg.DrawItemStruct.hDC, ColorToRGB(clWindowText));
end;
FillRect(Msg.DrawItemStruct.hDC, Msg.DrawItemStruct.rcItem, 0);
TextOut(Msg.DrawItemStruct.hDC, 4, 4, '_no_selected_item_', 18);
SelectObject(Msg.DrawItemStruct.hDC, Font);
end;
end;

I think you want the normal thing, to display something in the ComboBox when no selection has yet been made. Instant of a blank rectangle. Imagine a form full of blank comboboxes... ;)
What I've seen most programmers do is have the first item as the title to display in the ComboBox.
So, in FormCreate (after you've populated the ComboBox), you set its ItemIndex to 0, and this displays the title.
In its OnChange event you can choose to take no action if item 0 is selected ("real" items then have base 1 for index), or get ItemIndex-1 and skip action if < 0.
Must be a super common complaint from everyone who has used Comboboxes the first time. I can't understand how none of the coders recognize it.
All Borland et al would have had to do was to initialize a new ComboBox with ItemIndex=0 and the confusion would have been gone. It's certainly not obvious that you have to set index 0 - since you see the blank line when clicked, the logical conclusion is that it has index 0. Probably they wanted to give designers the option to add a label outside the combobox instead.

Related

Accelerator keys for `TActionToolBar` not working

I cannot get the accelerator keys for a TActionToolBar to work.
This is what I am doing (reproducable in D2006, XE4):
Select New -> VCL Forms Application
Add ActionManager1 to the form
Add a new action Action1 in ActionManager1, set caption of action to &Test
Add ActionToolBar1 to the form
Add an item to ActionManager.ActionBars and set ActionManager.ActionBars[0].ActionBar to ActionToolBar1
Add an item to ActionManager.ActionBars[0].Items and set Action to Action1
Set the Action1.OnExecute event to show a message
Start program --> toolbar is displayed just fine and works via mouse
Press ALT+T --> nothing happens, but a Ding sound
What step am I missing?
As the existing answer points out, action toolbars do not support this functionality.
My personal opinion is that, this has been overlooked. Toolbar buttons often showing images instead of text might be one reason to do so (at least it was for me). However, evidently, toolbar buttons have the functionality when they show their captions, so could the action toolbar buttons.
#Silver points out in a comment that action bars have the capability to find accelerated items. In fact action menus use that functionality. Same functionality could easily be integrated into TCustomForm.IsShortCut for action toolbars, which already iterates action lists to find possible shortcut targets.
We can override the method and do it ourselves. Below example gives priority to default handling so assigned shortcuts will suppress keyboard accelerators with the same character, but this logic could easily be reversed.
function TForm1.IsShortCut(var Message: TWMKey): Boolean;
var
Item: TActionClientItem;
i: Integer;
begin
Result := inherited IsShortCut(Message);
if not Result and (KeyDataToShiftState(Message.KeyData) = [ssAlt]) then begin
for i := 0 to ActionManager1.ActionBars.Count - 1 do begin
if ActionManager1.ActionBars[i].ActionBar is TActionToolBar then begin
Item := TActionToolBar(ActionManager1.ActionBars[i].ActionBar)
.FindAccelItem(Message.CharCode);
if Assigned(Item) and Item.ShowCaption and Assigned(Item.Action)
and Item.Action.Execute then begin
Result := True;
Break;
end;
end;
end;
end;
end;
It seems that accelerator keys are not implemented for TActionToolBar - so no steps missing.
The following is not a real solution but a workaround that adds shortcuts by parsing the captions of the action (thanks to the suggestion of #KenWhite). A real solution for the question you will find in the accepted answer. I'll keep that answer for reference anyway:
uses System.Actions, System.UiTypes, Vcl.Menus, Vcl.ActnMan;
procedure AddShortCutsFromActionCaption(AActionMan: TActionManager);
var
Act: TContainedAction;
AccelKey: string;
I: Integer;
begin
for I := 0 to AActionMan.ActionCount - 1 do
begin
Act := AActionMan.Actions[I];
if Act.ShortCut = 0 then
begin
AccelKey := GetHotKey(Act.Caption);
if AccelKey <> '' then
Act.ShortCut := TextToShortCut('Alt+' + AccelKey);
end;
end;
end;
AddShortCutsFromActionCaption must be run once for ActionManager1 after the localization is run. That way the different accelerator keys for different languages remain functional.
If a shortcut already exists or if the caption of the action is modified, this workaround will not work - but for my purposes this is okay.

Determine if text has been selected that may be cut or copied to clipboard

My app has menu items for cut, copy and paste. I know how to execute these actions but not how to determine if something has been selected. I need to know this to enable or disable the cut and copy menu items (which I would do in TAction.OnUpdate events).
For example, to copy selected text from the currently focused control, I use this:
if Assigned(Focused) and TPlatformServices.Current.SupportsPlatformService(IFMXClipboardService, Svc) then
if Supports(Focused.GetObject, ITextActions, TextAction) then
TextAction.CopyToClipboard;
But, how do I determine if any selection of text has been made in the currently focused control?
I could iterate through all my controls and use conditions like this:
if ((Focused.GetObject is TMemo) and (TMemo(Focused.GetObject).SelLength > 0) then
<Enable the Cut and Copy menu items>
but this does not seem elegant. Is there a better way?
EDIT:
Based on Remy's answer, I programmed the following and it seems to work:
procedure TMyMainForm.EditCut1Update(Sender: TObject);
var
Textinput: ITextinput;
begin
if Assigned(Focused) and Supports(Focused.GetObject, ITextinput, Textinput) then
if Length(Textinput.GetSelection) > 0 then
EditCut1.Enabled := True
else
EditCut1.Enabled := False;
end;
EditCut1 is my TAction for cut operations, and EditCut1Update is its OnUpdate event handler.
EDIT 2:
Following Remy's comment on my first edit, I am now using:
procedure TMyMainForm.EditCut1Update(Sender: TObject);
var
TextInput: ITextInput;
begin
if Assigned(Focused) and Supports(Focused.GetObject, ITextInput, TextInput)
then
EditCut1.Enabled := not TextInput.GetSelectionRect.IsEmpty;
end;
TEdit and TMemo (and "all controls that provide a text area") implement the ITextInput interface, which has GetSelection(), GetSelectionBounds(), and GetSelectionRect() methods.

How to know when the USER changed the text in a TMemo/TEdit?

I was always bugged by the fact that TMemo (and other similar controls) only have the OnChange event. I would like to know when the USER changed the text, not when the text was changed programmatically.
I know two methods to discriminate between the user changed text and programmatically changed text:
Put OnChange:= NIL before you change the text programmatically. Then restore the OnChange. This is error prone as you need to remember to do it every time you change text from the code (and to which memos/edits needs this special treatment to be applied). Now we know that every time the OnChange is called, the control was edited by user.
Capture the OnKeyPress, MouseDown, etc events. Decide if the text was actually changed and manually call the code that needs to be called when user edited the ext. This could add a big amount of procedures to an already large file.
There is a more elegant way to do it?
You can write a helper procedure to do your option 1, and use it in your framework whenever you want to ensure no OnChange event is triggered when you set the text. e.g.:
type
TCustomEditAccess = class(TCustomEdit);
procedure SetEditTextNoEvent(Edit: TCustomEdit; const AText: string);
var
OldOnChange: TNotifyEvent;
begin
with TCustomEditAccess(Edit) do
begin
OldOnChange := OnChange;
try
OnChange := nil;
Text := AText;
finally
OnChange := OldOnChange;
end;
end;
end;
TMemo has also the Lines property which also triggers OnChange, so you can make another similar procedure that accepts Lines: TStrings argument.
How about using the Modified property?
procedure TForm1.MyEditChange(Sender: TObject);
begin
if MyEdit.Modified then
begin
// The user changed the text since it was last reset (i.e. set programmatically)
// If you want/need to indicate you've "taken care" of the
// current modification, you can reset Modified to false manually here.
// Otherwise it will be reset the next time you assign something to the
// Text property programmatically.
MyEdit.Modified := false;
end;
end;

Multiselect using mouse in TListview?

Say you have a form with a TListView, with MultiSelect enabled. Normally you have to press shift or control to select multiple items. If I'd like to have the listview select/de-select additional items with only a mouse-click, how do I do that?
Like if you click item1 and then item3 both would be selected, and then if you click item1 again, it would leave only item3 selected.
I don't see any built in properties in the Object Inspector that look relevant. Do I need to modify my ListviewOnMouseDown or ListviewOnSelectItem to change the selection?
This kind of selection is not implemented in listview controls, maybe because they support checkboxes which can be independently checked, I don't know...
You have to modify the behavior yourself. But using OnMouseDown or OnSelectItem events are not really appropriate, as the selection have already been carried out by the time they are fired. Below example intercepts left mouse button down message.
type
TListView = class(vcl.comctrls.TListView)
protected
procedure WMLButtonDown(var Message: TWMLButtonDown);
message WM_LBUTTONDOWN;
end;
procedure TListView.WMLButtonDown(var Message: TWMLButtonDown);
begin
Message.Keys := MK_CONTROL;
inherited;
end;
You can intercept the message by any other means, assigning to WindowProc, deriving a new control... Of course you can also implement the behavioral change conditionally, or would like to test and preserve other virtual keys/buttons. See documentation in that case.
A ListView does not natively support what you are asking for. You will have to maintain your own list of "selected" items, using the OnClick or OnMouseDown event to detect which item the user is clicking on so you can toggle the contents of your list accordingly and then reset the ListView's selection to match your updated list as needed.
Set the ExtendedSelect property of the ListView to False.
Update:
The ListView does not have an ExtendedSelect property. It is only available for the ListBox.
But it would be possible to add that to the ListView. Here's an improved version of the code posted by Sertac that adds ExtendedSelect. I also improved it so it is a bit more user friendly than the original because it keeps the shift key for multi selection working. (I hope I may post that improved version here, it is a bit easier to read than in my comment).
type
TListView = class(Vcl.ComCtrls.TListView)
private
FExtendedSelect: Boolean;
procedure SetExtendedSelect(const Value: Boolean);
protected
procedure WMLButtonDown(var Message: TWMLButtonDown);
message WM_LBUTTONDOWN;
public
property ExtendedSelect: Boolean read FExtendedSelect write SetExtendedSelect;
end;
procedure TListView.SetExtendedSelect(const Value: Boolean);
begin
FExtendedSelect := Value;
end;
procedure TListView.WMLButtonDown(var Message: TWMLButtonDown);
begin
if not FExtendedSelect then
begin
if Message.Keys and MK_CONTROL <> 0 then
Message.Keys := Message.Keys and (not MK_CONTROL)
else if Message.Keys and MK_SHIFT = 0 then
Message.Keys := MK_CONTROL;
end;
inherited;
end;

Delphi: How to make ENTER key works as TAB key in TFrame

I have a Frame and some controls on them (edits, buttons, etc.). How to intercept pressing of ENTER key anywhere on a frame control and translate in to TAB key (taking into account SHIFT status)?
All you need is to modify the handling of CM_DIALOGKEY.
type
TMyForm = class(TForm)
protected
procedure CMDialogKey(var Message: TCMDialogKey); message CM_DIALOGKEY;
end;
procedure TMyForm.CMDialogKey(var Message: TCMDialogKey);
begin
if Message.CharCode=VK_RETURN then
Message.CharCode := VK_TAB;
inherited;
end;
Well, it's pretty obvious what this does and how it works.
You ask how to do this in a frame. It's not possible to handle dialog navigation in a frame. That's done by the form for fairly obvious reasons. So you'll need somehow to splice this code into the form that hosts your frame.
For a memo control this will have no effect. They will treat pressing ENTER as input of a line break. But I presume that's what you would wish to happen. Otherwise the memo control would be completely unusable.
Here's some example code that would handle a message on the frame to be able to navigate to the next control when Enter is pressed. Note that this sample does not modify the Enter key to become a Tab key. Instead it selects the next control and prevents further processing of the key down message.
Also note that the code may require further tweaking. One for, if any of the controls actually need to process the Enter key, for instance a TMemo, you need to add an exception. Second for, the navigation is wrapped in the frame, i.e. after the last frame control the first frame control is focused - not a control on the form and not on the frame. For these, you might want to add conditions for the message return, if you want default processing on some condition simply call inherited without doing any other thing.
type
TFrame2 = class(TFrame)
...
protected
procedure CMChildKey(var Message: TCMChildKey); message CM_CHILDKEY;
end;
..
procedure TFrame2.CMChildKey(var Message: TCMChildKey);
begin
if Message.CharCode = VK_RETURN then begin
SelectNext(Screen.ActiveControl, not Bool(GetKeyState(VK_SHIFT) and $80), True);
Message.Result := 1;
end else
inherited;
end;
I think there is a lot of "drop and forget" components to do it, for example on http://Torry.net
For instance, such a component was part of RxLib and later was inherted in JediVCL with TJvEnterAsTab name.

Resources