Show notification numbers on tab items - delphi

I have a tab-oriented Firemonkey multi-platform application. I have some tabs which might have individual notifications. I'm aware of the Notification Center for mobile apps, but internally within my own app, I'm not sure how to do a similar indicator on tab items.
For example, I have a TTabControl with 4 tabs. Each tab has a custom icon. On each tab next to the icon, I would like to show a notification indicator, similar to that shown on the application icon when using Notifications.
Something like this:
How can I show such an indicator on tabs for iOS and Android?
I've tried putting a label on it, but there's no way of knowing how to programatically position that label when the form is resized / rescaled / rearranged for different platforms. If the TTabItem had a Position and Size property, together I could position such a label. But without it, how can I position anything relevant to this tab?
PS - Please pardon my horrific graphics :-)

I figured out how to position a label over a tab item. It's a bit sloppy, but it works. First of all, the TTabControl.TabPosition must NOT be PlatformDefault. It must be either Top or Bottom. Secondly, the label must reside in the same parent control as the TTabControl. Then, implement a function like so:
procedure TfrmMain.PositionNotifyLabel(ALabel: TLabel; ATabItem: TTabItem);
var
TabLeft, TabTop: Single;
X: Integer;
T: TTabItem;
TabControl: TTabControl;
begin
TabControl:= ATabItem.TabControl;
//Calculate left position of tab
TabLeft:= 0;
for X := 0 to TabControl.TabCount-1 do begin
T:= TabControl.Tabs[X];
if T = ATabItem then Break;
TabLeft:= TabLeft + T.Width;
end;
case TabControl.TabPosition of
TTabPosition.Top: begin
TabTop:= 0;
end;
TTabPosition.Bottom: begin
TabTop:= TabControl.Height - TabControl.TabHeight;
end;
else begin
//Not Supported
TabTop:= 0;
end;
end;
//Calculate label width based on tab width
ALabel.Width:= ATabItem.Width / 3;
//Position label to calculated Left/Top positions
ALabel.Position.X:= TabLeft + ATabItem.Width - ALabel.Width;
ALabel.Position.Y:= TabTop;
ALabel.BringToFront;
end;
Call this function on form show and on form resize like so:
PositionNotifyLabel(lblCartNotify, tabCart);

Related

Make footer bar disappear on scrolling in a Delphi / Firemonkey app

I have a simple Delphi / Firemonkey app that has a lot of data within a TListView. To maximize the available space on screen, I want to make the footer bar disappear with animation if the user scrolls down. Just the same way the LinkedIn app does (at least on Android) for example. Is there an easy way to get there? Some mechanism that is integrated in then Firemonkey framework (like TMultiView)?
I'm using Delphi 11.2.
Thank you!
In FireMonkey you can achieve this quite easily by using Animations like TFloatAnimation which you can attach to any visual FMX component.
For instance if your footer bar is a StatusBar this is how you will done it:
In Form design select your status bar component
Find TFloatAnimation component in your component palette and double click on it. This will add Float Animation to the status bar. If done correctly Float Animation will be shown as child component of your status bar.
Change the PropertyName property of the added Float Animation to point to StatusBar.Size.Height property. This tells the Float Animation that it will be updating Status bars height property.
Now write two new procedures which you can then call to fire the prepared animation when needed.
The procedures would look something like this:
procedure TForm2.HideStatusBar;
begin
//Store initial height of the status bar as start value so we can use it later
//for restoring status bar to initial size
FloatAnimation2.StartValue := StatusBar1.Height;
FloatAnimation2.StopValue := 0;
//Set inverste property to false.
FloatAnimation2.Inverse := False;
//Start the animation
FloatAnimation2.Start;
end;
procedure TForm2.ShowStatusBar;
begin
//In order to reverse animation we need to set Inverse property to True
//We don't store any StartValue or StopValue since we have done that already
//when we decided to hide the status bar
FloatAnimation2.Inverse := True;
//Start the reverse animation
FloatAnimation2.Start;
end;
The reason why I store initial height of the status bar within HideStatsBar procedure instead of setting it at design time is to allow scenarios where you may allow your users to change the size of the said status bar.
NOTE: If your footer bar is a panel with Bottom alignment be aware that animating panel breaks the alignment and other components don't update their size properly as they should. I haven't figured out why this is happening and how to fix it.
Thanks to the help of SilverWarior, I got it finally working perfect just for my needs. To apply to Tom Brunberg's questions I will provide a full solution here including SilverWarior's slightly modified code of course.
I'm using the "HeaderFooter Template" of Delphi 11.2 but had to change the footer TToolbar to a bottom-aligned TPanel. It did not work with a TToolbar on Android. I placed the TListview in the middle of the form.
procedure THeaderFooterForm.FormShow(Sender: TObject);
begin
// just add some testing data
ListView1.BeginUpdate;
for var iCnt := 0 to 4711 do
ListView1.Items.Add.Text:='ListView Item ' + iCnt.ToString;
ListView1.EndUpdate;
FScrollPos:=0;
end;
procedure THeaderFooterForm.ListView1ScrollViewChange(Sender: TObject);
begin
var iNewPos:=ListView1.ScrollViewPos;
if FScrollPos > iNewPos then
begin
HeaderLabel.Text:='OnScroll Up';
Self.ShowStatusBar;
end
else
begin
HeaderLabel.Text:='OnScroll Down';
Self.HideStatusBar;
end;
FScrollPos:=iNewPos
end;
procedure THeaderFooterForm.HideStatusBar;
begin
// hiding the footer if height is already zero makes the footer simply do
// nothing on Android (but works in Win64)
if Footer.Height < 2 then
Exit;
// store initial height of the status bar as start value so we can use it later
// for restoring status bar to initial size
FloatAnimation1.StartValue := Footer.Height;
FloatAnimation1.StopValue := 0;
// start the animation
FloatAnimation1.Start;
// just for debugging
HeaderLabel.Text:=HeaderLabel.Text + ' AnimHideStart';
end;
procedure THeaderFooterForm.ShowStatusBar;
begin
// showing the footer if already visible makes it simply do nothing (but works in Win64)
if Footer.Height > 40 then
Exit;
// using the (far more elegant) .Invers machanism did not work
FloatAnimation1.StartValue := 0;
FloatAnimation1.StopValue := 48;
// start the animation
FloatAnimation1.Start;
// just for debugging
HeaderLabel.Text:=HeaderLabel.Text + ' AnimShowStart';
end;
This minimal example code placed within the form works on Windows 11 x64 and x86 and on my Android 13 device. Would be interesting on an iOS device of course.

Display second form as full screen with slide animation

I have a simple application that contain 2 forms, both forms has these properties:
BorderStyle := bsnone;
WindowState := wsMaximized;
I set these properties to make both of them full screen. In the main form there is a button and when I click I want to show the second form as full screen with slide animation so I used this code:
AnimateWindow(form2.Handle, 500, AW_ACTIVATE OR AW_SLIDE OR AW_HOR_NEGATIVE);
Form2 is in auto create and visible property is set to false.
The problem when I tried this I saw odd results, the animation play but the form2 appear without any controls and not covering the full screen.
How to fix that so I can display form2 as full screen with slide animation ?
I am using XE5
Based on MSN
https://msdn.microsoft.com/en-us/library/windows/desktop/ms632669(v=vs.85).aspx
there are lots of problems reported of using this function. So I recomend you go and implement the animation yourself.
Since you are interested only in sliding information do next:
Fist change your form size to fit into the monitor size.
MyForm.Width := Screen.Width;
MyForm.Height := Screen.Height;
Then move your form to the edge of the screen you want the animation to start from. When doing so keep atleast one pixel of the form inside the visible area of the monitor.
//Strating animation from left border
MyForm.Left := 1 - MyForm.Width;
MyForm.Top := 0;
//Starting from right border
MyForm.Left := MyForm.Width - 1;
MyForm.Top := 0;
//Starting from top border
MyForm.Left := 0;
MyForm.Top := 1 - MyForm.Height;
//Starting from bottom border
MyForm.Left := 0;
MyForm.Top := MyForm.Height - 1;
Once your have positioned your form in starting possition make it visible and enable timer that will beused to update forms position multiple times (animate) until it gets into desired position
MyForm.Show;
AniTimer.Enabled;
And start animation which is basically just updating your form position by using a simple timer
//Left to right animation
procedure MyForm.AniTimerOnTimer(Sender: TObject);
//Constant used to define by how many pixels will the form be moved
//on each timer interval
const MoveStep: Integer = 5;
begin
if MyForm.Left < MoveStep then
begin
MyForm.Left := MyForm.Left + MoveStep;
end;
else
begin
MyForm.Left := 0;
AniTimer.Enabled := False;
end;
end;
Use similar approach for other directions if needed.
Instead of defining MoveStep as constant you can make it as a variable and then dynamically calculate its value so that animation is finished in N steps.
MoveStep := Screen.Width div N;
If you would like to have diagonal animation you would need two MoveStep variables. One for horizontal and one for vertical axis. And you need to make sure that both are being calculated in order to finish animation in specific number of steps
MoveStepX := Screen.Width div N;
MoveStepY := Screen.Height div N;
So now you can controll your animation speed by changing MoveStep and timer interval.
Note I don't recomend setting timer interval to small. Why?
As ypu probably know TTimer component isn't known for its acruacy so it could lead to noticable speed variation of of your animation.
Also changing form position multiple times woulrd require part of it being rerendered hwen it comes into visual rage so it could generate significant load to the CPU.
Moving fomr a few less times and with larger increments could greatly reduce the CPU load whle still performing adequate aniamtion smothness.
So do some testing to find the best combination of timer interval and move step.
Your problem is, before Form2 is first shown, the VCL does not create API windows of the windowed controls. Because it doesn't need to. Remember 'visible' is still set to false when you call AnimateWindow.
Below is a not very elegant workaround which sets 'visible' while the form has 0 width and height. It also addresses an additional problem which I don't know why you are not having. It is that I cannot animate a maximized window at all, which seems logical to me - a maximized window does not move. Anyway, to test it I suggest setting 'wsNormal' as WindowState at design time.
procedure TForm1.Timer1Timer(Sender: TObject);
begin
if Form2.Visible then begin
Form2.WindowState := wsNormal;
AnimateWindow(Form2.Handle, 500, AW_HIDE OR AW_SLIDE OR AW_HOR_NEGATIVE);
Form2.Close;
end else begin
Form2.Width := 0;
Form2.Height := 0;
Form2.Visible := True;
ShowWindow(Form2.Handle, SW_HIDE);
Form2.WindowState := wsNormal;
Form2.Width := Form2.Monitor.Width;
Form2.Height := Form2.Monitor.Height;
AnimateWindow(form2.Handle, 500, AW_ACTIVATE or AW_SLIDE OR AW_HOR_NEGATIVE);
Form2.WindowState := wsMaximized;
end;
end;
AnimateWindow is a bit of a loner. It doesn't play nicely with the rest of the team in Delphi. Even though the MSDN doc for it says that it shows the form it actually doesn't do it properly. It only does the animation. I suppose it would do it nicely if you went all out Windows API and knew all that is required.
A few things to keep in mind:
I suppose you noticed that the slide effect doesn't show properly
with the borders enabled.
It doesn't know about the WindowState property of the form so it will not Maximize the form if you wanted it to.
It doesn't show controls after the call, only graphic controls
It knows nothing of Delphi and how Delphi handles the showing and
hiding of its forms
So the trick is:
Take away borders like you have done.
Before showing the form for the first time you must specify its size.
Seeing that you want it maximized, just set it to the screen size
where it will display and set its position to the four corners of
that screen. This can be done on Form2's OnCreate.
So upon clicking the button you first call the AnimateWindow then call normal Form2.Show
There might be other fixes but this is the one I know of.

How do I make two controls each occupy half their parent's area?

I have an application that has a sidebar attached to it (TPanel --> alRight), that uses a CategoryPanel (alClient) inside of it. This CategoryPanel has exactly 2 Groups which are non aligned. I would like to share the boundary of these 2 groups so it would fill up the whole panel space in a 50/50 ratio. Unfortunately The CategoryGroups do not support alignment in designtime, which forces me to run my application every time I want to test it. I tried to set each CategoryGroup to the height to half of the panel, but it displays scrollbars. (See Picture 2)
How can I align/share the boundary in a ratio of 50/50 properly?
According to your comments, you want to run this code:
procedure TForm1.UpdateGroupHeights;
begin
if not CategoryPanel1.Collapsed then
CategoryPanel1.Height := CategoryPanelGroup1.ClientHeight div 2;
if not CategoryPanel2.Collapsed then
CategoryPanel2.Height := CategoryPanelGroup1.ClientHeight -
CategoryPanelGroup1.ClientHeight div 2;
end;
whenever anything changes that you wish to affect the layout of your groups. So I think you need to call this function from the following events:
The OnCreate event of the form.
The OnResize event of the TCategoryPanelGroup.
The OnCollapse and OnExpand events of the two category panels.
That looks a bit weird though when one panel is collapsed, and the other is expanded. Personally I'd rejig the code to fill all available space.
if not CategoryPanel1.Collapsed then
;//nothing to do
if CategoryPanel1.Collapsed and not CategoryPanel2.Collapsed then
CategoryPanel2.Height := CategoryPanelGroup1.ClientHeight-CategoryPanel1.Height;
if not CategoryPanel1.Collapsed and CategoryPanel2.Collapsed then
CategoryPanel1.Height := CategoryPanelGroup1.ClientHeight-CategoryPanel2.Height;
if not CategoryPanel1.Collapsed and not CategoryPanel2.Collapsed then
begin
CategoryPanel1.Height := CategoryPanelGroup1.ClientHeight div 2;
CategoryPanel2.Height := CategoryPanelGroup1.ClientHeight-CategoryPanel1.Height;
end;

String Grid using Styles in Delphi XE2 - Scroll bar doesn't update

I'm making a simple application in Delphi XE2 which uses specifically the "Carbon" style. There's a large String Grid which has thousands of rows. I have a process which loops through the records of this grid, does some work, and makes some changes in the grid. As the process loops, the row which is being currently processed gets highlighted (by setting TStringGrid.Row).
The problem is that when I apply the style to this grid, the scroll bar doesn't change position as the row is changed. The loop does properly highlight each row as it's being processed, but by the time it gets towards the end of the list, the scroll bar on the right is still all the way at the top.
How do I make the grid's scroll bar move along with it?
Here's a sample of how I'm looping:
procedure TForm1.Button1Click(Sender: TObject);
var
X: Integer;
begin
FStop:= False;
for X:= 1 to Grid.RowCount - 1 do begin
if FStop then Break; //Ability to stop loop
Grid.Row:= X; //Highlight current row
DoSomeLenghyWork;
ChangeSomethingOnGrid;
Application.ProcessMessages; //Keep program responding
end;
end;
Everything works just fine when I'm not using any styles.
If invalidate and repaint don't do anything for you, try resizing the string grid:
Grid.Width := Grid.Width - 1;
Grid.Width := Grid.Width + 1;
Try playing with the string grid options that hide and show the scrollbars. Hide them before you update and show them after. Perhaps that will force them to repaint.
Try moving the scroll position and moving it back to the original position.
This worked for me - it forces windows to repaint the border area of the StringGrid:
SetWindowPos(Grid.Handle, 0, 0, 0, Grid.Width, Grid.Height, SWP_DRAWFRAME);

Image Preview in a Listbox

How can I display a preview (almost like a hint) of an image when I hover the mouse over an item in a listbox of filenames? I've tried showing a form and loading the image, but when the preview form shows, I lose focus for the listbox which means that when I move the mouse, the preview image does not change when I go to the next item in the list.
Thanks, Pieter.
I have, based on the answer from TOndrej, tried to implement a custom THintWindow, but the Canvas.StretchDraw does not draw the bitmap sent as a parameter. Any ideas why not? Text is displayed normally.
procedure TFormMain.DisplayPreview(HintImage: TBitmap);
var
CustomHint: THintWindow;
Rect: TRect;
MousePoint: TPoint;
begin
*{
Based on Source: http://www.chami.com/tips/delphi/112996D.html
}*
GetCursorPos(MousePoint);
with Rect do
begin
// set the position and size of the hint window
Left := MousePoint.X;
Top := MousePoint.Y;
Right := Left + 50;
Bottom := Top + 25;
end;
CustomHint := THintWindow.Create(Self);
try
with CustomHint do
begin
// set the background color
//Color := clNone;
**Canvas.StretchDraw(Rect, HintImage);**
ActivateHint(Rect, 'Hint');
Application.ProcessMessages;
//
// perform your tasks here
// before closing the hint window
//
Sleep(500);
ReleaseHandle;
end;
finally
if Assigned(CustomHint) then
CustomHint.Free;
end;
end;
To me it seems you want a custom hint window. To do this you should write a new THintWindow descendant and either set it globally to the whole application by assigning your new class to the HintWindowClass global variable in Forms unit, or write your own listbox descendant in which you will handle CM_HINTSHOW message and assign your new hint window class to HintInfo.HintWindowClass. (HintInfo points to a record passed to your control in the CM_HINTSHOW message by the VCL.)
1) Are you displaying your preview form like a dialog (Modal Window) if yes then change it to non modal window.
2) Remember to set focus back to your parent window once the preview form shows up, that way your parent form that has the listbox has the focus and it will pass the mouse move events to the listbox.
Best Regards.

Resources