Blog

All Blog Posts  |  Next Post  |  Previous Post

Get social with TMS list controls

Friday, August 6, 2010

Downloads

TMS Software Delphi  Components Twitter client

TMS Software Delphi  Components Facebook client


Introduction
Friends lists, tweets lists, comments lists, posts lists, favourites lists, ... we tend to order everything into lists in our life. How we want to visualize items in lists has dramatically evolved from the nineties Windows 3.0 Listbox control that could just display text. Web pages render lists with items that have all kinds of attributes. At TMS, we thought that Windows development with Delphi also deserved richer list controls and recently developed two such powerful list controls: TAdvSmoothListBox and TMS Poly List controls. As an excercise for the versatility & flexibility of these controls, we created two sample applications that we'd like to share: an application that shows your tweets and an application that shows your Facebook home page. For the Twitter viewer, we used the TAdvSmoothListBox. For the Facebook home page, we used the TMS Poly List controls. The focus of this article is to explain how these list controls were used. The two sample applications have 3 selectable styles to demonstrate how easy it is to change the look of the TMS list controls.



General approach

The applications presented here have basically 3 parts, handled with 3 components. The Twitter viewer uses our TRESTClient component to retrieve data via the Twitter API in JSON format. The JSON data is parsed with a basic JSON parser, the component TJSONFormat and the data is visualized with the TAdvSmoothListBox. The Facebook home page viewer also uses the TRESTClient component for data retrieval in JSON format, the TJSONFormat component for parsing and a TAdvVerticalPolyList for showing the home page and a TAdvPolyList for showing the friends list.
The TRESTClient implements HTTP GET and HTTP POST via the Microsoft Windows WinInet API. We have choosen for the WinInet API as it is stable, it is able to pick up Internet Explorer's proxy settings transparently and it does not introduce dependencies on other components and is as such easy to use in a wide range of Delphi versions. The TRESTClient returns data as a TStream or string. It can equally work to download an image or text. It also handleds basic HTTP authentication. If there is an interest in this component, we might make it available at a later time. The data is parsed by a JSON parser we developed. This is a small & simple JSON parser that parses the JSON tree and in the process can also generate JSON from an object tree. We might also consider to make this available as a component if there is interest for this.

The Twitter client

The Twitter client as such uses the Twitter API (http://apiwiki.twitter.com/Twitter-API-Documentation) to access Twitter feed information in JSON format. The TAdvSmoothListBox is very convenient to display a Tweet. Following items are displayed inside the TAdvSmoothListBoxItem:

a) The item's Caption of the item is set to the screen_name of the user sending the Tweet
b) The item's Info is set to the creation timestamp of the Tweet
c) The item's left graphic element is set to the user's profile image. In a separate thread, these images are loaded
d) The item's Notes are set to the TWeet's text. The Notes can handle HTML formatted text, so this is very convenient to display & handle links in the Tweets. The ConvertTextToHTML event converts the Tweet's text to HTML and takes care of the encoding of special characters in the Tweet text
e) A clickable hyperlink displayed in the item's notes.


The TRESTClient currently uses basic HTTP authentication with the Twitter API. We'll replace this basic authentication in a next version with OAuth authentication, but currently the code it uses is:

    with RESTClient1.Items.Add do
    begin
      url := 'http://api.twitter.com/1/account/verify_credentials.json';
      HTTPUserID := username;
      HTTPPassword := password;
    end;
    RESTClient1.Execute;
When the RESTClient has a valid response, we can load the JSON data in the parser:
     jsonformat1.LoadFromStream(RESTClient1.Items[0].Stream);
and retrieve the profile screen and profile image URL via the parser to assign it in the list header:
     AdvSmoothListBox1.Header.Caption := jsonformat1.JSONObject.GetJSONValueString('screen_name');
      url := jsonformat1.JSONObject.GetJSONValueString('profile_image_url');
The next step is to retrieve the timeline of the user with the RESTClient
      RESTClient1.Items.Clear;
      with RESTClient1.Items.Add do
      begin
        url := 'http://api.twitter.com/1/statuses/home_timeline.json';
        HTTPUserID := username;
        HTTPPassword := password;
      end;
      RESTClient1.Execute;
This returns a JSON array of tweet objects. The code loops through this array and loads one tweet returned in an JSON object js in the TAdvSmoothListBox with:
          A := TStringList.Create;
          Split(' ', js.GetJSONValueString('created_at'), A);
          AdvSmoothListBox1.Items[i].Info := 'Tweet on ' +  A[2] + ' ' + A[1] + ' ' + A[5] + ' ' + A[3];
          A.Free;
          AdvSmoothListBox1.Items[i].InfoFont.Style := [fsItalic];
          AdvSmoothListBox1.Items[i].NotesFont.Size := 10;
          AdvSmoothListBox1.Items[i].NotesLocation := plCenterLeft;
          AdvSmoothListBox1.Items[i].Notes := ConvertTextToHTML(ProcessString((js as TJSONObject).GetJSONValueString('text')));
          AdvSmoothListBox1.Items[i].GraphicLeftType := gtImage;
          AdvSmoothListBox1.Items[i].GraphicLeftWidth := 45;
          AdvSmoothListBox1.Items[i].GraphicLeftHeight := 45;
For the images, we create an object that holds the ID and the URL of the profile image and store it in the item's associated object Item.ItemObject.
          itId := TItemID.Create;
          itId.ID := js.GetJSONValueNum('id');
          itId.ImageURL := jsUser.GetJSONValueString('profile_image_url');
          AdvSmoothListBox1.items[i].ItemObject := itId;
A separate thread processes the URLs by looping through all items and obtaining the URL of the profile picture via the ItemObject. It uses the RESTClient to download the image and assign it to the Item's GraphicLeft property. This is the code the separate profile picture loader thread performs:
            for i := 0 to AdvSmoothListBox1.Items.Count - 1 do
            begin
              if Assigned(AdvSmoothListBox1.Items[i].ItemObject) then
              begin
                itemid := TItemID(AdvSmoothListBox1.Items[i].ItemObject);
                if itemid.Status = tsNone then
                begin
                  itemid.Status := tsStarted;
                  url := itemid.ImageURL;		  
                  img := DownloadPicture(url);
                  if Assigned(img) then
                  begin
                    AdvSmoothListBox1.Items[i].GraphicLeft.Assign(img);
                    img.Free;
                  end;
                  itemid.Status := tsLoaded;
                end;
              end;
            end;
That is basically all that is needed to create a basic viewer of your tweet timeline via the Twitter API and have it presented in a nice way via the TAdvSmoothListBox.

The Facebook client

The Facebook client uses the Facebook graph API (http://developers.facebook.com/docs/api). For authentication, first a Facebook application ID must be requested. Then, with this ID, the user needs to authenticate access to his profile. Upon succesful authentication, an access token is used for every access to the API. To obtain the access token, we need to use a TWebBrowser control to show the Facebook login screen and the redirect URL holds the access token. After login, the application loads the facebook friends list in a TAdvPolyList. This is a grid of images with the name of the friends shown under each image. The Facebook home page is shown in a TAdvVerticalPolyList. We have choosen for the TAdvVerticalPolyList as it enables to embed controls in each item. The controls are used to open comments for an item or to open a link associated with a home page item. The item type that is added in the TAdvVerticalPolyList is of the type TImageSectionItem. This allows us to associate an image with each item, to have a larger text caption and to have the description of the item. As a posted item can also have an additional image, this is is custom drawn on the item. Further, a panel can be associated with the item that can hold a button to view comments or visit a link.

The Facebook home page

Organisation of the facebook home page item:

a) Username as caption of the polylist TImageSectionItem
b) Post text as description of the TImageSectionItem. The margin of the description is set to the width of the post image to have the description text properly indented.
c) Picture of the user who posted the item. This picture is loaded via a separate thread as the TImageSectionItem's picture that is left positioned
d) Image of the post that is custom drawn in the OnItemEndDraw event
e) Panel embedded in the item with Comments & Visit link button
f) Comments items. These are two items of the type TImageItem with the user image positioned right. The OnItemStartDraw event is used to paint a text bubble like background.

This is the code that creates the item for a normal home page post:
    itNormal := TImageSectionItem(List.AddItem(TImageSectionItem));
    with itNormal do
    begin
      // reserve space for poster image
      ImageHeight := 50;
      ImageWidth := 50;
      // set item default height
      Height := 75;
      CaptionSize := 11;
      Ellipsis := True;
      WordWrap := False;
      DescriptionMargin.Left := 60;
      CaptionMargin.Left := 50;
      CaptionColor := ItemCaptionColor;
      DescriptionColor := ItemDescriptionColor;
      // set the caption to poster name and its location top left in the item
      Caption := msgFromName;
      CaptionLocation := tlTopLeft;
      // set the description text for the TImageSectionItem
      if msgMessage <> '' then
      begin
        if (msgDescription = '') and (msgcaption = '') then
          Description := msgMessage
        else
          Caption := Caption + ' ' + msgMessage;
      end;
      if msgCaption <> '' then
        Description := msgCaption + #13#10;
      if msgDescription <> '' then
        Description := Description + msgDescription + #13#10;
      // object associated with the item
      ItemObject := TItemID.Create;
      TItemID(ItemObject).ID := msgFromID;
      TItemID(ItemObject).MsgType := MsgType;
      TItemID(ItemObject).ItemType := itHome;
      LineStyle := psSolid;
      LineSize := 1;
      LineLocation := [llTop];
      LineColor := clSilver;
      // a picture is associated with the post, so download and 
      if msgPicture <> '' then
      begin
        with TItemID(ItemObject) do
        begin
          DescriptionImage := DownloadPicture(msgPicture);
          if Assigned(DescriptionImage) then
          begin
            Height := DescriptionImage.Height + 60;
	    // set indent for the description
            DescriptionMargin.Left := DescriptionMargin.Left + DescriptionImage.Width + 20;
          end;
        end;
      end;
      // assign the event handler that will draw the description image in the associated item object.
      OnItemEndDraw := ItemEndDraw;
     end;
To insert a comment, a TImageItem is used. It is initialized with image on the right side, the description is the text of the comment and the caption is the comment poster. The comments are inserted with Level set to 1, as opposed to Level 0 for normal items. Based on this Level property, the button to show hide/hide comments will make items with the corresponding level visible or not visible. Comment creation in code:
    itComment := TImageItem(List.AddItem(TImageItem));
    with itComment do
    begin
      Level := 1;
      // put images on the right side in the comment item
      ImageTextLayout := ilRightLeft;
      ImageHeight := 50;
      ImageWidth := 50;
      Height := 60;
      CaptionSize := 11;
      Ellipsis := True;
      WordWrap := False;
      CaptionColor := ItemCommentCaptionColor;
      DescriptionColor := ItemCommentDescriptionColor;
      Caption := msgFromName;
      // item's caption & description location
      CaptionLocation := tlTopLeft;
      DescriptionLocation := tlTopLeft;
      // indent for the comment poster & text 
      CaptionMargin.Left := 100;
      DescriptionMargin.Left := 110;
      if msgMessage <> '' then
        Description := msgMessage;
      // Object associated with the comment item
      ItemObject := TItemID.Create;
      TItemID(ItemObject).ID := msgFromID;
      TItemID(ItemObject).MsgType := MsgType;
      TItemID(ItemObject).ItemType := itHome;
      // code that will draw the item background as a balloon
      OnItemStartDraw := ItemStartDraw;
    end;
As shown in the code above, custom drawing was used for normal items and for comment items. For normal items, the item's picture is drawn on top of the item. This means, it is drawn after the default drawing was done, hence, it is done from the OnItemEndDraw event. For the comment, the background is custom drawn first and the default drawing of the comment item is done on top of this custom drawing, hence, the custom drawing is done from the OnItemStartDraw event. The code for these two event handlers is:

Draw the item's description image, retrieved from object associated with the TAdvVerticalPolyList item via the ItemObject property:
procedure TForm1.ItemEndDraw(Sender: TObject; AGraphics: TGPGraphics;
  Item: TCustomItem; ARect: TGPRectF);
var
  itemid: TItemID;
  pic: TAdvGDIPPicture;
  offset: Double;
begin
  if Assigned(item) then
  begin
    itemid := TItemID(item.ItemObject);
    if Assigned(itemid.DescriptionImage) then
    begin
      pic := itemid.DescriptionImage;
      offset := ARect.X + 40;
      if Assigned((Item as TImageSectionItem).Image) then
        offset := offset + (Item as TImageSectionItem).Image.Width + 5;
      pic.GDIPDraw(AGraphics, MakeRect(offset, ARect.Y + 5 + (ARect.Height -  pic.Height) / 2, pic.Width, pic.Height))
    end;
  end;
end;
Draw the background of a comment item as a balloon:
procedure TForm1.ItemStartDraw(Sender: TObject; AGraphics: TGPGraphics;
  Item: TCustomItem; ARect: TGPRectF);
var
  b: TGPSolidBrush;
  pth: TGPGraphicsPath;
begin
  b := TGPSolidBrush.Create(MakeColor(255, ItemBackGroundColor));
  AGraphics.FillRectangle(b, MakeRect(ARect.X + 100, ARect.Y, ARect.Width - 100, ARect.Height));
  pth := TGPGraphicsPath.Create;
  pth.AddLine(ARect.X + 104, ARect.Y, ARect.X + 110, ARect.Y - 8);
  pth.AddLine(ARect.X + 110, ARect.Y - 8, ARect.X + 116, ARect.Y);
  pth.CloseFigure;
  AGraphics.FillPath(b, pth);
  pth.Free;
  b.Free;
end;


The Facebook friends list

To show the friends list, a TAdvPolyList is used. This is a grid of poly list items. Each item is a picture with under the picture the name of the Facebook friend. To retrieve the friends list, the RESTClient uses the Friends request:
    RESTClient1.Items.Clear;
    RESTClient1.Items.Add.URL := 'https://graph.facebook.com/me/friends?access_token=' + access_code; ;
    RESTClient1.Execute;
The retrieved JSON array is parsed with the JSONFormat component and in a loop, TImageItem objects are added to the TAdvPolyList. The TAdvPolyList.Columns property is set to 3. This means that irrespective of the number of rows, the items will always be organised in 3 columns in the TAdvVerticalPolylist. The code to loop through the JSON array and to create TImageItem objects and add these in the polylist is:
     var
       vl: TValueList;
       i: integer;
       jo: TJSONObject;

      JSONFormat1.LoadFromStream(RESTClient1.Items[0].Stream);
      vl := JSONFormat1.JSONObject.GetJSONValueArray('data');
      ProfileList.BeginUpdate;
      for i := 0 to vl.Count - 1 do
      begin
        jo := TJSONObject(vl.Items[i]);
        with TImageItem(ProfileList.AddItem(TImageItem)) do
        begin
          CaptionColor := ItemCaptionColor;
          Height := 80;
          CaptionSize := 7;
          ImageHeight := 30;
          ImageWidth := 30;
          Caption := ProcessString(jo.GetJSONValueString('name'));
          strid := jo.GetJSONValueString('id');
          ItemObject := TItemID.Create;
          TItemID(ItemObject).ID := strid;
          TItemID(ItemObject).ItemType := itProfile;
        end;
      end;
      ProfileList.EndUpdate;


Conclusion

Both demo applications were created as a fun project under the "eat your own dogfood" principle as a validation for both the experience for developing with the TMS TAdvSmoothListBox and TMS Poly List controls and the experience in the user interface. We found little or no limitations or friction to achieve the UI features we wanted to offer in these samples nor with the code to implement it. The executable applications are here so you can play with it. We look forward to your feedback about the list controls in general and the possible interest in the RESTClient and JSON parser. If there is sufficient interest and when the RESTClient & JSON parser are further polished for general use, we might consider to publish the full source code of the application. Stay tuned.

Bruno Fierens




This blog post has received 14 comments.


1. Friday, August 6, 2010 at 10:32:16 AM

These demo''s look great. Well done.

Birger Jansen


2. Friday, August 6, 2010 at 10:50:54 AM

Would love to get the TRestClient and JSON parser from you guys someday in the future.

Thanks.

Marc Pike


3. Friday, August 6, 2010 at 11:56:44 AM

Great article, I would love to be able to have the full source.

Thanks,

Howard Mike


4. Friday, August 6, 2010 at 3:19:45 PM

Will you be releasing the source code to these shortly? I would love to see the full source. Many thanks Chris

O''Connor Chris


5. Friday, August 6, 2010 at 9:10:05 PM

I am VERY interested in this application source code. And I am especially interested in the JSON parser.

Thanks for all the hard work,

Keith

Tolbert Keith


6. Monday, August 9, 2010 at 5:30:18 AM

This looks great. I would really like to get my hands on the TRestClient and the JSON parser - even if you don''t polish it :o)

Jan

Jan


7. Tuesday, August 10, 2010 at 4:25:50 PM

Very interested in seeing the full source code.

Bacus James


8. Tuesday, August 17, 2010 at 8:48:54 AM

I am also super interested in the full source code.

Napoli Marco


9. Wednesday, August 18, 2010 at 11:46:41 AM

Yes, I would be interested in the full source.

Lisenby Larry


10. Friday, August 20, 2010 at 6:31:49 PM

Geat job Bruno! and of course, I would be interested in the full source.


Martin Alcaraz


11. Monday, August 23, 2010 at 2:42:46 PM

I would also be very interested in the TRestClient and the JSON parser. Full source for these apps would be excellent too.

Cleland Bryan


12. Saturday, October 9, 2010 at 12:29:46 PM

Cool apps Bruno, I would also love the source code and hope that you guys will release the components in the near future.

van Ingen Michiel


13. Friday, March 4, 2011 at 2:00:48 PM

Any news on making the full source available ?

David Rose


14. Saturday, March 5, 2011 at 3:09:22 AM

Sorry, this project got sidetracked a bit due to workload with other projects. We''ll try to pick this up again as soon as possible.

Bruno Fierens




Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post