Delphi - Custom drawing a message list
Please refer to my question asked at tek-tips.com: http://tek-tips.com/viewthread.cfm?qid=1663735&page=1
As I mentioned in a couple of my other threads, I'm building a control to pretty much replicate the SMS text messaging on the iPhone. This consists of simply a bubble on either side of the control containing text. I already have a working version, but need to re-build it from scratch. I'd like some advice on some things...
What do you think is the best method to store the list of message data? I was thinking using a TCollection, but that could be way too heavy. Currently I'm using a TStringList containing raw text data which is parsed out and translated appropriately. This works great because I don't have to create any extra objects with loads of unnecessary properties. It's just...
data syntax:
<user_size><deliminator><user><message_size><deliminator><message>
which could look like:
9|djjd4713023|This is a test message!
characters:
SDTTTTTTTTTSSDTTTTTTTTTTTTTTTTTTTTTTT
user_size = 9
deliminator = |
user = djjd47130
etc.......
Anyway, I expect possibly thousands of messages in this control. Which brings me to my next question. The best way to draw it. Currently, I'm using a TDrawGrid, and am in the process of converting it to a TStringGrid so I can contain the text directly in the grid rather than the TStringList. However that's where I stopped because I'm wondering if there's another better way than to use a gr开发者_JAVA技巧id. It's easy because it automatically manages storing the rect of each cell, etc.
How about using a TImage instead? There's another concern about the largest possible control size. This control automatically grows higher with the more messages, so again, if there's for example 1,000 messages, with an average message bubble height of about 80 pixels, that would mean the grid control needs to be 80,000 pixels high. Using a TImage though could be tough, because I would then have to manually calculate the position on that canvas to draw each balloon, similar to how grids internally keep track of that.
By the way, this grid (or otherwise canvas) is inside of a TScrollBox (final control will inherit from a TScrollingWinControl). This is how it can scroll, while the actual canvas its self is much larger than the control, big enough to draw all the message balloons. Scrolling in the control is actually moving up and down in the TScrollBox to see portions of the control canvas displaying the messages.
To summarize the pieces I need to perfect: - Light-weight method of storing message items in a list (inside grid, string list, collection, or other list?) - Scrollable canvas with list items of variable height (grid, image, or other list?) - Allowing maximum number of messages to be kept with variable heights? - Ability to customize how the control reacts to user actions to automatically scroll up or down
I'm not necessarily asking for a fix for anything, but rather advice to make it the best possible way.
If I were you, I'd do something like this:
unit ChatControl;
interface
uses
Windows, Messages, SysUtils, Classes, Controls, Graphics;
type
TUser = (User1 = 0, User2 = 1);
TChatControl = class(TCustomControl)
private
FColor1, FColor2: TColor;
FStrings: TStringList;
FScrollPos: integer;
FOldScrollPos: integer;
FBottomPos: integer;
FBoxTops: array of integer;
FInvalidateCache: boolean;
procedure StringsChanged(Sender: TObject);
procedure SetColor1(Color1: TColor);
procedure SetColor2(Color2: TColor);
procedure SetStringList(Strings: TStringList);
procedure ScrollPosUpdated;
procedure InvalidateCache;
protected
procedure Paint; override;
procedure Resize; override;
procedure CreateParams(var Params: TCreateParams); override;
procedure WndProc(var Message: TMessage); override;
function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
MousePos: TPoint): Boolean; override;
procedure Click; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function Say(const User: TUser; const S: String): Integer;
procedure ScrollToBottom;
published
property Align;
property Anchors;
property Cursor;
property Font;
property Color1: TColor read FColor1 write SetColor1 default clSkyBlue;
property Color2: TColor read FColor2 write SetColor2 default clMoneyGreen;
property Strings: TStringList read FStrings write SetStringList;
property TabOrder;
property TabStop;
end;
procedure Register;
implementation
uses Math;
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TChatControl]);
end;
{ TChatControl }
procedure TChatControl.Click;
begin
inherited;
if CanFocus and TabStop then
SetFocus;
end;
constructor TChatControl.Create(AOwner: TComponent);
begin
inherited;
DoubleBuffered := true;
FScrollPos := 0;
FBoxTops := nil;
InvalidateCache;
FStrings := TStringList.Create;
FStrings.OnChange := StringsChanged;
FColor1 := clSkyBlue;
FColor2 := clMoneyGreen;
FOldScrollPos := MaxInt;
end;
procedure TChatControl.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.Style := Params.Style or WS_VSCROLL;
end;
destructor TChatControl.Destroy;
begin
FStrings.Free;
inherited;
end;
function TChatControl.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
MousePos: TPoint): Boolean;
begin
dec(FScrollPos, WheelDelta);
ScrollPosUpdated;
end;
procedure TChatControl.InvalidateCache;
begin
FInvalidateCache := true;
end;
procedure TChatControl.Paint;
const
Aligns: array[TUser] of integer = (DT_RIGHT, DT_LEFT);
var
Colors: array[TUser] of TColor;
var
User: TUser;
i, y, MaxWidth, RectWidth: integer;
r, r2: TRect;
SI: TScrollInfo;
begin
inherited;
Colors[User1] := FColor1;
Colors[User2] := FColor2;
y := 10 - FScrollPos;
MaxWidth := ClientWidth div 2;
Canvas.Font.Assign(Font);
if FInvalidateCache then
SetLength(FBoxTops, FStrings.Count);
for i := 0 to FStrings.Count - 1 do
begin
if FInvalidateCache then
FBoxTops[i] := y + FScrollPos
else
begin
if (i < (FStrings.Count - 1)) and (FBoxTops[i + 1] - FScrollPos < 0) then
Continue;
if FBoxTops[i] - FScrollPos > ClientHeight then
Break;
y := FBoxTops[i] - FScrollPos;
end;
User := TUser(FStrings.Objects[i]);
Canvas.Brush.Color := Colors[User];
r := Rect(10, y, MaxWidth, 16);
DrawText(Canvas.Handle,
PChar(FStrings[i]),
Length(FStrings[i]),
r,
Aligns[User] or DT_WORDBREAK or DT_CALCRECT);
if User = User2 then
begin
RectWidth := r.Right - r.Left;
r.Right := ClientWidth - 10;
r.Left := r.Right - RectWidth;
end;
r2 := Rect(r.Left - 4, r.Top - 4, r.Right + 4, r.Bottom + 4);
Canvas.RoundRect(r2, 5, 5);
DrawText(Canvas.Handle,
PChar(FStrings[i]),
Length(FStrings[i]),
r,
Aligns[User] or DT_WORDBREAK);
if FInvalidateCache then
begin
y := r.Bottom + 10;
FBottomPos := y + FScrollPos;
end;
end;
SI.cbSize := sizeof(SI);
SI.fMask := SIF_ALL;
SI.nMin := 0;
SI.nMax := FBottomPos;
SI.nPage := ClientHeight;
SI.nPos := FScrollPos;
SI.nTrackPos := SI.nPos;
SetScrollInfo(Handle, SB_VERT, SI, true);
if FInvalidateCache then
ScrollToBottom;
FInvalidateCache := false;
end;
procedure TChatControl.Resize;
begin
inherited;
InvalidateCache;
Invalidate;
end;
function TChatControl.Say(const User: TUser; const S: String): Integer;
begin
result := FStrings.AddObject(S, TObject(User));
end;
procedure TChatControl.ScrollToBottom;
begin
Perform(WM_VSCROLL, SB_BOTTOM, 0);
end;
procedure TChatControl.SetColor1(Color1: TColor);
begin
if FColor1 <> Color1 then
begin
FColor1 := Color1;
Invalidate;
end;
end;
procedure TChatControl.SetColor2(Color2: TColor);
begin
if FColor2 <> Color2 then
begin
FColor2 := Color2;
Invalidate;
end;
end;
procedure TChatControl.SetStringList(Strings: TStringList);
begin
FStrings.Assign(Strings);
InvalidateCache;
Invalidate;
end;
procedure TChatControl.StringsChanged(Sender: TObject);
begin
InvalidateCache;
Invalidate;
end;
procedure TChatControl.WndProc(var Message: TMessage);
var
SI: TScrollInfo;
begin
inherited;
case Message.Msg of
WM_GETDLGCODE:
Message.Result := Message.Result or DLGC_WANTARROWS;
WM_KEYDOWN:
case Message.wParam of
VK_UP:
Perform(WM_VSCROLL, SB_LINEUP, 0);
VK_DOWN:
Perform(WM_VSCROLL, SB_LINEDOWN, 0);
VK_PRIOR:
Perform(WM_VSCROLL, SB_PAGEUP, 0);
VK_NEXT:
Perform(WM_VSCROLL, SB_PAGEDOWN, 0);
VK_HOME:
Perform(WM_VSCROLL, SB_TOP, 0);
VK_END:
Perform(WM_VSCROLL, SB_BOTTOM, 0);
end;
WM_VSCROLL:
begin
case Message.WParamLo of
SB_TOP:
begin
FScrollPos := 0;
ScrollPosUpdated;
end;
SB_BOTTOM:
begin
FScrollPos := FBottomPos - ClientHeight;
ScrollPosUpdated;
end;
SB_LINEUP:
begin
dec(FScrollPos);
ScrollPosUpdated;
end;
SB_LINEDOWN:
begin
inc(FScrollPos);
ScrollPosUpdated;
end;
SB_PAGEUP:
begin
dec(FScrollPos, ClientHeight);
ScrollPosUpdated;
end;
SB_PAGEDOWN:
begin
inc(FScrollPos, ClientHeight);
ScrollPosUpdated;
end;
SB_THUMBTRACK:
begin
ZeroMemory(@SI, sizeof(SI));
SI.cbSize := sizeof(SI);
SI.fMask := SIF_TRACKPOS;
if GetScrollInfo(Handle, SB_VERT, SI) then
begin
FScrollPos := SI.nTrackPos;
ScrollPosUpdated;
end;
end;
end;
Message.Result := 0;
end;
end;
end;
procedure TChatControl.ScrollPosUpdated;
begin
FScrollPos := EnsureRange(FScrollPos, 0, FBottomPos - ClientHeight);
if FOldScrollPos <> FScrollPos then
Invalidate;
FOldScrollPos := FScrollPos;
end;
end.
This is ultra-fast even with 10 000 messages.
To test it, do something like
procedure TForm4.Button1Click(Sender: TObject);
var
i: integer;
begin
ChatControl1.Strings.Clear;
for i := 0 to StrToInt(LabeledEdit1.Text) - 1 do
ChatControl1.Say(TUser(Random(2)), RandomString(2, 80));
end;
procedure TForm4.Edit2KeyPress(Sender: TObject; var Key: Char);
begin
Assert(Sender is TEdit);
if ord(Key) = VK_RETURN then
begin
ChatControl1.Say(TUser(TEdit(Sender).Tag), TEdit(Sender).TExt);
Key := #0;
TEdit(Sender).Clear;
end;
end;
Full source and compiled demo: ChatControlDemo.zip
Still, there is certainly room for further improvements. For example, it is pretty stupid to recompute the entire cache array when you add a single message to the end of the string list. Clearly, it suffices to simply append the position of this newly added message to the cache array. But I leave that up to you.
精彩评论