Aug 01 Cover Story
Volume Number: 17 (2001)
Issue Number: 08
Column Tag: Mac OS X Technology
The Databrowser Control
by Jens Miltner
Learn to leverage the power of Finder-like list and column views
Introduction
While the main focus of the Carbon API set was to provide an easy upgrade path for existing "classic" Macintosh applications to Mac OS X, starting with CarbonLib 1.1, a number of new APIs were "ported back" from Mac OS X to be available in the Mac OS 8/9 version of Carbon.
One of the more prominent additions is the databrowser control, which allows programmers to add Finder-style outline or column views to their applications in a native look & feel. Up to now, there have been numerous ListManager replacements that support multiple levels of hierarchy. Databrowser pursuits to replace them with a native Mac OS control that's flexible enough to accommodate most applications' needs.
Figure 1 shows the databrowser control in both list and column view mode, although I'm sure most readers will already have seen the databrowser in the Mac OS X Finder.
Figure 1: Databrowser in Mac OS X, in column and list view mode.
In this article, I'll focus on using the more general databrowser features. Databrowser is a very rich control, so it's not possible to cover each and every feature in a single article, but I hope that after having read this article, you will feel comfortable enough with databrowser to give it a try if you aren't already using it.
General Concepts
Databrowser differs from the "regular" controls in a number of ways. The most obvious difference is its complexity. However, the most important difference is, that in contrast to the "classic" controls we all know since 1984, databrowser does not manage all the data itself, but instead calls back into your application whenever it needs some of the data being displayed.
This concept of abstracting the UI from the data model is very important and allows for a great variety of data being displayed as well as the timeliness of the displayed information. It also allows you, the programmer, to decide how you want to store your data. Whenever databrowser needs to access some data, it'll call back into your application and request data. It's the up to you to retrieve the data from your storage and convert it into a form that databrowser is able to use.
Databrowser identifies items (rows) by a unique 32-bit item ID. Each item has multiple properties, which are identified by a 32-bit property ID. There are a number of predefined properties (e.g. whether the item is a container, whether the item is mutable, whether the item is active, etc.) and Apple reserves the range [0..1024] for these predefined properties. The rest of the property ID space is available for custom properties, usually represented as columns in the databrowser, i.e. when databrowser needs to know about the data for a given column, it'll call your item accessor callback with the property ID you assigned to the column when you added it. Thus, when we speak about cells in databrowser, they are identified by item ID plus property ID. Apple's Technote on databrowser (TN 2009) also illustrates this.
Databrowser supports a number of different display types for the data, including (among others): text, icons, date/time, checkboxes, etc. (Figure 1 shows an example of different column types). It'll also track user interactions with the cells automatically (if the cell in question is modifiable): for example, it allows the user to toggle checkboxes, make choices from popup menus and do inline editing of text.
Another important fact is that databrowser only knows about visible items (visible in the sense of ‘theoretically visible', i.e. they don't have to be on the screen, but they must be revealed in the hierarchy). As soon as you close a container, the items inside the container are removed and databrowser forgets about them. When you add items to a container, that container is automatically opened. Again, it's your task to remember the item hierarchy, but this also gives you more freedom in where you store information about the hierarchy (e.g. when browsing a file system, the hierarchy is already available in the file system itself).
As containers are opened or closed (either programmatically or by the user), databrowser will send you notifications. You should respond to the "container opened" notification by adding all the items that are inside the container.
To find out which items are containers, databrowser will just call your item accessor callback to request the "item is container" property for the item in question. If the item is a container, databrowser will display the disclosure control and allow you to add items into a subhierarchy of this item.
One nice detail about databrowser is that notifications are sent in a very consistent manner, so you can rely on them to always show up, independent of what caused the underlying action. For example, you'll receive a notification whenever an item is being removed. Now, an item may be removed implicitly because the item's container was closed or removed, which causes databrowser to remove all items inside this container. Or an item may be removed because you called the API to remove that item. Or the item may have been implicitly removed because you disposed the databrowser control.
Databrowser will send you the "item was removed" notification in any of these situations for any item that was removed (and of course in the correct order, i.e. first the children then the parent). The nice thing about this is that you can use this notification e.g. to clean up a cache you maintained for the item's properties and you can rely on the fact that this notification will be sent eventually, so this is the only place you'd need to implement code for cache cleanup.
What's in there for you?
By using the databrowser control to display your data, you ensure a consistent user interface experience for your customers: Every Mac user knows Finder's hierarchical file list view and a substantial number of users will become used to Mac OS X's Finder's column view, so this is how many of them would expect the look-and-feel of other hierarchical browsers to be. Therefore, Finder certainly sets a standard for displaying data in list form on the Macintosh.
Until databrowser came along, applications used to provide a more-or-less complete copy of the Finder's hierarchical table view. While most of them looked very much like the Finder, there were slight differences in the behavior, which introduced yet another modal state. More important, though, is the fact that the Aqua look of databrowser on Mac OS X now is very different from the appearance on Classic Mac OS, so all those applications would need to rev their code and adjust to the new Aqua browser look and feel if they want to provide a consistent UI experience.
If you use databrowser for your list and column views, you isolate yourself from the details and differences in look and feel on each platform, and instead can concentrate on implementing the algorithms for gathering and modifying the data being displayed. Leave the details on how to properly display the data on each platform to the Apple engineers! No more playing catch up with the latest UI changes on the next version of the OS!
And, of course: the next time your fellow Windows programmers try to tease you about how Mac programmers have to assemble everything from tiny bits and pieces, show them this beautiful beast - and don't forget to ask them about a system control that can display hierarchical lists and multiple columns ;-)
The Details
In the next section, I'd like to show how you actually go about creating and configuring a databrowser so it'll present your data in the way you want.
Don't fear the API
In order to manage the complex tasks that are involved when displaying arbitrary data, the databrowser APIs consist of 150+ functions (if you take a look at ControlDefinitions.h, you'll find that the databrowser APIs make up for half of the header file).
Now, don't be overwhelmed by the size of this API set: For your basic databrowser, you will need only a few (maybe 10-20) of these functions. The rest of the API set is there to allow you to customize databrowser to your needs.
You will also notice that databrowser uses a very consistent and explanatory API naming convention, so for many issues, the name of an API or constant will already tell you a lot about its purpose.
Also, for the purpose of event handling, a databrowser control really behaves like any regular control, i.e. you use the standard ControlManager APIs like HandleControlKey, HandleControlClick, DrawOneControl, etc., to pass events to the control. It's just that for a databrowser control, these APIs take on their true low-level meaning rather than the high-level semantic meaning commonly associated with their traditional usage.
So, what is necessary?
There are a few steps that will be the same regardless of how you use databrowser. Later in this article, we'll walk through some sample code that shows how to use some of databrowser features, so you'll be able to see this in source code.
Creation
First of all, you need to create the control using the CreateDataBrowserControl API (instead of using the generic NewControl with overloaded parameters):
ControlRef browserControlRef;
err = CreateDataBrowserControl(
browserWindow, // a WindowRef
&controlBounds, // a Rectangle
kDataBrowserListView,
&browserControlRef);
This will create a databrowser control in list view mode (of course, you can also create the databrowser in column view mode).
The next step will be to tell the databrowser control how to call you when it needs to request or update your data or just notify you about important events. This is done by passing a struct full of callback function pointers to the databrowser. There is one struct for the basic callbacks. This one is required in order the get databrowser to work:
struct DataBrowserCallbacks {
UInt32 version; // Use kDataBrowserLatestCallbacks
union {
struct {
DataBrowserItemDataUPP
itemDataCallback;
DataBrowserItemCompareUPP
itemCompareCallback;
DataBrowserItemNotificationUPP
itemNotificationCallback;
DataBrowserAddDragItemUPP
addDragItemCallback;
DataBrowserAcceptDragUPP
acceptDragCallback;
DataBrowserReceiveDragUPP
receiveDragCallback;
DataBrowserPostProcessDragUPP
postProcessDragCallback;
DataBrowserItemHelpContentUPP
itemHelpContentCallback;
DataBrowserGetContextualMenuUPP
getContextualMenuCallback;
DataBrowserSelectContextualMenuUPP
selectContextualMenuCallback;
} v1;
} u;
};
The itemDataCallback is mandatory - databrowser just won't work if it can't access your items' data.
Without the itemCompareCallback, databrowser won't be able to sort the items in your databrowser, so they would remain in the order in which they've been added. Since there are only very few situations where this is desired, most databrowser clients will actually implement this callback.
Unless your databrowser is only used to display a flat hierarchy or you won't allow the user to undisclose items, the itemNotificationCallback is also mandatory, since this is where you receive notifications about containers being opened, so you can fill them with their child items. Possible notifications include (among others) information about items being added or removed, begin and end of inline text editing sessions, change of selection, change of the user state (when the user modifies the column layout and/or sort specification), begin and end of a sort operation. You are not obliged to react to a notification, so you can safely ignore those you're not interested in.
The rest of the callbacks supply additional functionality that you may or may not want to implement (drag & drop, help tags and contextual menu support).
The SetDataBrowserCallbacks API will tell databrowser to use your callbacks (or, if you pass NULL, to clear previously set callbacks).
Another struct contains the custom databrowser callbacks. These are used to implement custom content properties, i.e. properties that don't use one of databrowser's built-in display styles. One notable custom content property is the preview property, which is used to display the preview area in column view mode (for an example, see Finder on Mac OS X: in column view mode, whenever you select a file, you'll see information about the file, plus a possible preview in the preview area). A discussion of custom content properties would go beyond the scope of this article, so I'll just introduce the structure defining the custom content callbacks:
struct DataBrowserCustomCallbacks {
UInt32 version; // Use kDataBrowserLatestCustomCallbacks
union {
struct {
DataBrowserCustomDrawUPP
drawItemCallback;
DataBrowserCustomEditUPP
editTextCallback;
DataBrowserCustomHitTestUPP
hitTestCallback;
DataBrowserCustomTrackingUPP
trackingCallback;
DataBrowserCustomDragRgnUPP
dragRegionCallback;
DataBrowserCustomAcceptDragUPP
acceptDragCallback;
DataBrowserCustomReceiveDragUPP
receiveDragCallback;
} v1;
} u;
};
This set of callbacks will be transferred to databrowser using the SetDataBrowserCustomCallbacks API (again allowing you to clear previously set callbacks by passing NULL).
Configuration
Now your databrowser knows how to request data, but it doesn't yet know how to display data once it's available. Therefore, the next step would be to tell databrowser about the column layout (assuming you told databrowser to work in list view mode): for each column you specify what kind of information should be displayed (text, icons, date/time, checkboxes, progress bars, relevance ranking bars, sliders). You also specify information about formatting the column's cells and assign unique IDs to the columns, which databrowser will use to identify the columns.
All this information is packaged in a structure called DataBrowserListViewColumnDesc. The first part of the struct is yet another struct that describes the custom property:
struct DataBrowserPropertyDesc {
DataBrowserPropertyID propertyID;
DataBrowserPropertyType propertyType;
DataBrowserPropertyFlags propertyFlags;
};
Here you specify which property ID is used to identify the column, you specify the column type and also some property flags, which govern how this column behaves with respect to sorting, moving, editing, etc.
The property ID can be any number >= 1024. The range [1..1024] is reserved for special properties that databrowser needs to query, which don't necessarily correspond to visual data (e.g. whether an item is a container, whether an item is editable, whether an item is active). Property ID 0 is defined to be interpreted as "no property".
The propertyType member specifies how the data is being displayed for this column. The available column types are:
- kDataBrowserTextType for text-only columns,
- kDataBrowserIconType for icon-only columns,
- kDataBrowserIconAndTextType for columns containing an icon followed by some text,
- kDataBrowserDateTimeType for columns showing a date and/or time,
- kDataBrowserSliderType for columns displaying a slider control,
- kDataBrowserCheckboxType for columns displaying a checkbox control,
- kDataBrowserProgressBarType for columns displaying a progress bar,
- kDataBrowserRelevanceRankType for columns displaying a relevance ranking bar and
- kDataBrowserPopupMenuType for columns displaying a popup menu.
Note that the column type only specifies how the item data is presented, not what the column header itself looks like.
The most important flags you can specify are:
kDataBrowserPropertyIsMutable to specify that the item data displayed in this column may be modified. If you don't specify this flag, databrowser will treat all data displayed in this column as read-only, and your itemData callback will never be called to change the data for this property. So, for example, if you want to let the user toggle the checkboxes in a checkbox column, you'll have to specify this property flag. (Note that modifying your cell's data won't be enabled unless both the column is declared mutable and your itemData callback returns true for the kDataBrowserItemIsEditableProperty)
kDataBrowserListViewMovableColumn to specify that the column may be moved to a different position by the user.
kDataBrowserListViewSortableColumn to specify that the databrowser may be sorted by this column. If this flag is specified, clicks into the column header will make this column the sort property and cause a re-sort of the databrowser (clicking an already sorted column again will revert the sort order).
kDataBrowserListViewSelectionColumn to specify that cells in this column should reflect/display the selection state (i.e. will be highlighted when selected) in minimal highlight mode (which is the default for list view mode). This flag also specifies which columns are included in the drag image (for minimal highlight mode).
There's another batch of property flags that specifies variants of specific column types, e.g. to have checkboxes toggle between 3 states, to specify how your date/time columns should display the date/time and to specify how the cell content is to be truncated in case it does not fit into the column width.
The second part of the DataBrowserListViewColumnDesc struct used to specify a column is a struct called DataBrowserListViewHeaderDesc, which specifies list view specific properties of the column:
struct DataBrowserListViewHeaderDesc {
UInt32 version;
UInt16 minimumWidth;
UInt16 maximumWidth;
SInt16 titleOffset;
CFStringRef titleString;
DataBrowserSortOrder initialOrder;
ControlFontStyleRec btnFontStyle;
ControlButtonContentInfo btnContentInfo;
};
For the version field, you should always specify the constant kDataBrowserListViewLatestHeaderDesc.
The minimumWidth and maximumWidth fields specify the minimum resp. maximum width of the column that can be achieved when the user resizes the column. Of course, specifying the same value for both fields prevents the user from resizing this column, making this a fixed width column.
The titleOffset field specifies how many pixels the column title (and also the cell contents) will be inset, allowing you to fine-tune the column layout.
The titleString field, of course, specifies the text to be displayed in the column header. After having added the column, you are responsible for releasing the string if you allocated it. You may pass NULL if you don't want the column to get a title.
The initialOrder field specifies what sort ordering to use when the user selects this column as the sort property for the first time. Databrowser will remember the last sort order, so subsequent selections of this column as the sort property will return to the last sort ordering.
In the btnFontStyle field, you can specify font, size, style and justification to use to display the column header (if you want to change the display of the databrowser content, use SetControlFontStyle). This struct is the regular ControlFontStyleRec struct used to tell controls about font and color settings, but currently databrowser ignores any color settings. You should make sure, though, to set up the flags field correctly, in case databrowser is going to support colors in the future.
Finally, the btnContentInfo field lets you specify what kind of content is drawn for the header button. Currently, the only variation supported is to draw an icon by passing an IconRef. This is in addition to the text specified by the titleString field, so you also have the option of text only and icon + text column headers.
Once you set all this up, you pass the structure to the databrowser control using the AddDataBrowserListViewColumn API.
Contents
So, that's it... but wait, there's no data yet!? Of course, you'll need to tell databrowser which data to display. We'll get into more detail as we walk through the sample code later on, but basically, this involves calling the AddDataBrowserItems API.
So, once again, don't shy away from the sheer amount of APIs. After our step-by-step tour through the sample code, you should be fairly comfortable with the basic tasks of setting up a databrowser to display your data - believe me, there's no voodoo or black magic involved, it's just a matter of getting accustomed to the way databrowser works.
Example: A Simple To-Do-List
Let's have a look at some code that shows how to use the databrowser control. In this case, a databrowser control is used to display and manage a simple to-do list (Figure 1 is actually a screenshot of the sample application). Unfortunately, the scope of this article doesn't allow me to cover all the nifty features available with databrowser, so the example focuses on the basic tasks (creating, configuring and filling a databrowser). However, the sample code also implements some slightly more advanced behaviors (drag&drop, help tags, preview area in column view mode), but I won't be able to go into detail about them, so you may just want to have a look at the source code, too.
All the databrowser related functionality is contained in the file ToDoListWindow.cpp. The class used to store information about a single task in the to-do list is implemented in ToDoItem.cpp, but this class only serves as the data container and does not interact with databrowser itself.
The sample application is completely CarbonEvent driven. For more information about CarbonEvents, you may want to have a look at Apple's documentation. Note, though, that you are not required to use CarbonEvents to drive databrowser. If you are using the "classic" WaitNextEvent approach, you just have to make sure to dispatch the appropriate events to databrowser - just like with any other control.
Before getting started, you'll have to make sure you set up your development environment correctly so that all the required APIs are available. If you are developing on Mac OS X, you're already set up correctly, but if you are developing on Mac OS 9, you'll need the Carbon SDK, which can be downloaded from Apple's website (http://developer.apple.com/sdk).
All of the definitions and declarations about the databrowser control can be found in the Universal Interfaces header file ControlDefinitions.h. As of this writing, there are two different declarations for some of the structs and types. The macro NO_DATA_BROWSER_TWEAKS is used to toggle between the two definitions. Always make sure this macro to be defined to be 0, since this will give you the API set that will continue to exist whereas the other API version is deprecated.
Throughout the sample code, error handling will be done by using C++ exceptions, most of the time by the use of the ThrowIfOSErr or ThrowOSErr functions, the former raising the exception only if the error passed is not noErr, the latter raising an exception using whatever error code is passed.
Of course, since C++ exceptions cannot be thrown across callback boundaries, it's mandatory to set up the appropriate catch handlers in the callback entry points to ensure that no uncaught exception will make its way out of the callback entry point (in which case your app would either silently terminate or just crash, depending on the C++ exception handling implementation).
How a databrowser is born
Obviously, the first thing you'll have to do is create your databrowser control. Since databrowser loves to live within an embedding hierarchy, on Mac OS 9, you'll have to create a root control manually (on Mac OS X, this is done for you by the OS). In the sample code, a control property is used to store the object of class ToDoListWindow with the databrowser control, so it can be retrieved in the databrowser callbacks:
Listing 1: Databrowser control creation
// create the root control (could be conditional for Mac OS 9 only,
// but shouldn't harm if done on X)
//
ThrowIfOSErr(CreateRootControl(
mWindowRef,
&rootControl));
// create a databrowser control in list view mode
// that spans the entire window
//
ThrowIfOSErr(CreateDataBrowserControl(
mWindowRef,
GetWindowPortBounds(mWindowRef,
&browserBounds),
kDataBrowserListView,
&mBrowser));
// attach a pointer to this object as a property of the browser control,
// so we can retrieve the object in the databrowser callbacks
//
{
ToDoListWindow* me(this);
ThrowIfOSErr(SetControlProperty(mBrowser,
kToDoListWindowPropertyCreator,
kToDoListWindowPropertyTag,
sizeof(me),
&me));
}
// set up our callback structure
//
DataBrowserCallbacks callbacks;
InitializeDataBrowserCallbacks(
&callbacks,
kDataBrowserLatestCallbacks);
callbacks.u.v1.itemDataCallback =
sItemDataUPP;
callbacks.u.v1.itemCompareCallback =
sItemCompareUPP;
callbacks.u.v1.itemNotificationCallback =
sItemNotificationUPP;
callbacks.u.v1.addDragItemCallback =
sAddDragItemUPP;
callbacks.u.v1.acceptDragCallback =
sAcceptDragUPP;
callbacks.u.v1.receiveDragCallback =
sReceiveDragUPP;
callbacks.u.v1.postProcessDragCallback =
sPostProcessDragUPP;
callbacks.u.v1.itemHelpContentCallback =
sItemHelpContentUPP;
//
// and install the callbacks in the databrowser control
//
ThrowIfOSErr(SetDataBrowserCallbacks(
mBrowser,
&callbacks));
There are two more callbacks for handling contextual menus for the databrowser contents that are not used in this sample code. The call to InitializeDataBrowserCallbacks will take care of initializing them, so databrowser knows they are not used. For the rest of the callbacks the appropriate UPPs (Universal Proc Pointers) have already been allocated in static object members, e.g.
// initialize static member sItemDataUPP to be a UPP to our static
// member function ItemDataProc
//
DataBrowserItemDataUPP
ToDoListWindow::sItemDataUPP(
NewDataBrowserItemDataUPP(
ToDoListWindow::ItemDataProc));
Now that the databrowser control is allocated, there is one more thing to prepare so that the databrowser looks nice when covering the entire window: Since it's the only control in the window, we tell databrowser not to display a frame and focus rectangle. This is done using the SetControlData API (actually, it's one of the few things about databrowser that doesn't have a specific API):
Boolean frameAndFocus(false);
ThrowIfOSErr(
SetControlData(
mBrowser,
kControlNoPart,
kControlDataBrowserIncludesFrameAndFocusTag,
sizeof(frameAndFocus),
&frameAndFocus));
Specifying the Columns
Since we configured our databrowser to display a list view (aka Finder-style hierarchical view), we also need to tell databrowser what columns to display. This is done by calling AddDataBrowserListViewColumn, passing the struct describing the column's properties and behaviors described earlier. Listing 2 is a snippet from the ToDoListWindow class of the sample code. The DataBrowserListViewColumnDesc struct is configured for each column and passed to AddDataBrowserListViewColumn, passing kDataBrowserListViewAppendColumn as the column position so it'll be appended to the end of the column headers.
Note the usage of various formatting flags in the column description for the modification date column (the last column): we specify how the date should be displayed and to truncate the date/time display string in the middle if necessary.
Since we want to support a deep hierarchy of to-do items, we have to tell databrowser which column should contain the disclosure control (using the SetDataBrowserListViewDisclosureColumn API). In this case (as in most applications) we chose it to be the first column, which we also make unmovable. However, you may specify any column to contain the disclosure control.
For the title column, we don't want to rely on databrowser's default algorithm of determining the column width based on minimum and maximum width, so after adding this column we explicitly specify the column width using SetDataBrowserTableViewNamedColumnWidth.
For some reason, the default row height in Mac OS X 10.0 is a little too narrow, so we'll tell databrowser to use a row height of 18 pixels, which looks a lot better.
Finally, if we have a previously saved user state, we'll restore it, so the column layout is exactly how the user left it last time (the user state includes the order of the column, the column widths and the current sorting).
Listing 2: ToDoListWindow::ConfigureListView ()
ToDoListWindow::ConfigureListView ()
Configure the databrowser list view by adding all our columns
void ToDoListWindow::ConfigureListView (void)
{
DataBrowserListViewColumnDesc columnDesc;
// fill in version of column description we know
//
columnDesc.headerBtnDesc.version =
kDataBrowserListViewLatestHeaderDesc;
// no icons in our header buttons
//
columnDesc.headerBtnDesc.btnContentInfo.
contentType = kControlContentTextOnly;
// Unfortunately, there's a bug on Mac OS 9 that requires us to
// explicitely specifiy the header button font,
// so we'll specify to use the list view font.
//
columnDesc.headerBtnDesc.btnFontStyle.flags
= kControlUseFontMask | kControlUseJustMask;
columnDesc.headerBtnDesc.btnFontStyle.font =
kControlFontViewSystemFont;
// we don't want any extra margins in our columns
//
columnDesc.headerBtnDesc.titleOffset = 0;
// specify the default sort order when column is first sorted
//
columnDesc.headerBtnDesc.initialOrder =
kDataBrowserOrderIncreasing;
//
// the index column
//
// set up the column property description
//
columnDesc.propertyDesc.propertyID =
kIndexColumn;
columnDesc.propertyDesc.propertyType =
kDataBrowserTextType;
columnDesc.propertyDesc.propertyFlags =
kDataBrowserListViewSelectionColumn |
kDataBrowserListViewDefaultColumnFlags;
// and some information about the column width and justification
//
columnDesc.headerBtnDesc.minimumWidth = 0;
columnDesc.headerBtnDesc.maximumWidth = 80;
columnDesc.headerBtnDesc.btnFontStyle.just =
teFlushDefault;
// set up the button title (empty)
//
columnDesc.headerBtnDesc.titleString = NULL;
// append the column to the end of the databrowser
//
ThrowIfOSErr(
::AddDataBrowserListViewColumn(
mBrowser, &columnDesc,
kDataBrowserListViewAppendColumn));
//
// this column will be the column to contain the disclosure control
//
ThrowIfOSErr(
::SetDataBrowserListViewDisclosureColumn(
mBrowser, kIndexColumn, false));
//
// the checkbox column
//
columnDesc.propertyDesc.propertyID =
kCompletedColumn;
columnDesc.propertyDesc.propertyType =
kDataBrowserCheckboxType;
columnDesc.propertyDesc.propertyFlags =
kDataBrowserPropertyIsMutable |
kDataBrowserListViewSortableColumn;
// column should not be resizable
//
columnDesc.headerBtnDesc.minimumWidth = 30;
columnDesc.headerBtnDesc.maximumWidth = 30;
// checkboxes look best when centered in the column
//
columnDesc.headerBtnDesc.btnFontStyle.just =
teCenter;
// no title
columnDesc.headerBtnDesc.titleString = NULL;
ThrowIfOSErr(
::AddDataBrowserListViewColumn(
mBrowser, &columnDesc,
kDataBrowserListViewAppendColumn));
//
// the title column
//
columnDesc.propertyDesc.propertyID =
kTitleColumn;
columnDesc.propertyDesc.propertyType =
kDataBrowserTextType;
columnDesc.propertyDesc.propertyFlags =
kDataBrowserPropertyIsMutable |
kDataBrowserListViewSelectionColumn |
kDataBrowserListViewDefaultColumnFlags;
columnDesc.headerBtnDesc.minimumWidth = 100;
columnDesc.headerBtnDesc.maximumWidth = 700;
columnDesc.headerBtnDesc.btnFontStyle.just =
teFlushDefault;
columnDesc.headerBtnDesc.titleString =
::CFStringCreateWithPascalString(
CFAllocatorGetDefault(),
"\pTask", kCFStringEncodingMacRoman);
ThrowIfOSErr(
::AddDataBrowserListViewColumn(
mBrowser, &columnDesc,
kDataBrowserListViewAppendColumn));
ThrowIfOSErr(
::SetDataBrowserTableViewNamedColumnWidth(
mBrowser, kTitleColumn, 200));
// since we allocated the string (not using CFSTR), we have to release it
CFRelease(columnDesc.headerBtnDesc.titleString);
//
// the percentage complete column
//
columnDesc.propertyDesc.propertyID =
kPercentageColumn;
columnDesc.propertyDesc.propertyType =
kDataBrowserProgressBarType;
columnDesc.propertyDesc.propertyFlags =
kDataBrowserListViewSortableColumn;
columnDesc.headerBtnDesc.minimumWidth = 30;
columnDesc.headerBtnDesc.maximumWidth = 100;
columnDesc.headerBtnDesc.btnFontStyle.just =
teFlushDefault;
// no title
columnDesc.headerBtnDesc.titleString = NULL;
ThrowIfOSErr(
::AddDataBrowserListViewColumn(
mBrowser, &columnDesc,
kDataBrowserListViewAppendColumn));
//
// the modification date column
//
columnDesc.propertyDesc.propertyID =
kLastModifiedColumn;
columnDesc.propertyDesc.propertyType =
kDataBrowserDateTimeType;
columnDesc.propertyDesc.propertyFlags =
kDataBrowserListViewSelectionColumn |
kDataBrowserListViewDefaultColumnFlags |
kDataBrowserDateTimeRelative
kDataBrowserDateTimeSecondsToo
kDataBrowserTruncateTextMiddle;
columnDesc.headerBtnDesc.minimumWidth = 80;
columnDesc.headerBtnDesc.maximumWidth = 280;
columnDesc.headerBtnDesc.btnFontStyle.just =
teFlushDefault;
columnDesc.headerBtnDesc.titleString
=::CFStringCreateWithPascalString(
CFAllocatorGetDefault(),
"\pLast Modified",
kCFStringEncodingMacRoman);
ThrowIfOSErr(
::AddDataBrowserListViewColumn(
mBrowser, &columnDesc,
kDataBrowserListViewAppendColumn));
CFRelease(columnDesc.headerBtnDesc.titleString);
// in MacOS X 10.0, the default row height is a little too small,
// so we'll bump this up a bit to get some decent vertical spacing.
//
ThrowIfOSErr(
SetDataBrowserTableViewRowHeight(
mBrowser, 18));
// finally, if there is a saved user state to restore, do so
//
if ( mListViewUserState )
{
SetDataBrowserUserState(mBrowser,
mListViewUserState);
CFRelease(mListViewUserState);
mListViewUserState = NULL;
}
}
How about some contents?
The last step will be to fill the databrowser with our data. There are two ways to fill a databrowser with items: You can either add the root level items manually, or you can define an invisible root item and just tell databrowser to use this root item as the databrowser target item.
You can think of the databrowser target item as the root container being browsed - think about how Finder displays a folder: the contents of the folder is displayed, but the actual root of the Finder window is the folder being displayed. In that case this folder would be the databrowser target. Specifying the databrowser target also allows you to keep track of where your hierarchy belongs. If you don't explicitly specify a target, databrowser uses a target ID of 0 (kDataBrowserNoItem) by default.
Now, while the first approach (adding all root items manually) seems like the logical thing to do at first, using the second method actually is much more elegant if you have a hierarchy to display: When you specify the databrowser target, databrowser will send you a ‘container opened' notification for the databrowser target item (i.e. the root container). Since you have to implement code to fill in a container's child items in your notification callback anyway, you can as well leverage this code to fill the root level by relying on the fact that databrowser will send you the ‘container opened' notification for the target item.
The sample code also takes this approach, so the final bit will be to call
ThrowIfOSErr(SetDataBrowserTarget(mBrowser, kRootItem));
That's it! If we already have items created at the root level, they will now appear in the databrowser as we respond to the ‘container opened' notification. From this point on, we'll let databrowser do its work just feeding it with events like any other control. Now we can focus on responding to the callbacks and working with our data.
The callback functions
Let's have a closer look at what's done in the databrowser callbacks. First, let's look at the itemData callback. Listing 3 shows the implementation for the itemData callback. This callback is used to request data, but also to communicate back data the user has modified by e.g. clicking a checkbox or editing the text of an item. In the sample code, the main entry point will split up between these two cases and dispatch to the OnGetItemData or the OnSetItemData member function, depending on whether this is a request for data or a notification about modified data:
Listing 3: Item data accessor callback
ToDoListWindow:: ItemDataProc()
Main entry point for the callback, will dispatch to the correct member
function depending on whether this is a load or store operation
pascal OSStatus ToDoListWindow::ItemDataProc(
ControlRef browser,
DataBrowserItemID item,
DataBrowserPropertyID property,
DataBrowserItemDataRef itemData,
Boolean setValue)
{
OSStatus err;
UInt32 ignoreActualSize;
ToDoListWindow* toDoListWindow;
// retrieve the ToDoListWindow object we attached to the browser
err = GetControlProperty(
browser,
kToDoListWindowPropertyCreator,
kToDoListWindowPropertyTag,
sizeof(toDoListWindow),
&ignoreActualSize,
&toDoListWindow);
if ( err == noErr ) try {
//
// let our window supply/store the data
//
bool handled;
if ( setValue )
handled = toDoListWindow->OnSetItemData(
item, property, itemData);
else
handled = toDoListWindow->OnGetItemData(
item, property, itemData);
//
// if it didn't handle the property, report an error
//
if ( !handled )
err = errDataBrowserPropertyNotSupported;
} catch( const COSErrException& e ) {
err = e.Cause();
}
return err;
}
ToDoListWindow::OnGetItemData()
The getter function for databrowser item properties.
Will return whether the requested property is supported.
bool ToDoListWindow::OnGetItemData(
DataBrowserItemID inWhichItem,
DataBrowserPropertyID inWhichProperty,
DataBrowserItemDataRef outItemData) const
{
switch ( inWhichProperty )
{
case kDataBrowserItemIsEditableProperty:
ThrowIfOSErr(
SetDataBrowserItemDataBooleanValue(
outItemData, true));
return true;
case kDataBrowserItemIsContainerProperty:
ThrowIfOSErr(
SetDataBrowserItemDataBooleanValue(
outItemData,
this->IsContainerItem(inWhichItem)));
return true;
case kDataBrowserItemSelfIdentityProperty:
{
// let our plain items have a clipboard icon,
// the container items get a folder icon, of course
//
IconRef icon;
if ( GetIconRef(
kOnSystemDisk,
kSystemIconsCreator,
this->IsContainerItem(inWhichItem) ?
kGenericFolderIcon :
kClipboardIcon,
&icon) == noErr )
{
ThrowIfOSErr(
SetDataBrowserItemDataIcon(
outItemData, icon));
ReleaseIconRef(icon);
}
}
// fall thru to kTitleColumn
case kTitleColumn:
// retrieve the title from the ToDoItem object
ThrowIfOSErr(
SetDataBrowserItemDataText(
outItemData,
this->ResolveItemID(inWhichItem)
->title));
return true;
case kIndexColumn:
{
char indexStr[256];
CFStringRef indexCFString(NULL);
// create a string representation of the index
sprintf(indexStr, "%u.",
GetItemIndex(
this->ResolveItemID(inWhichItem)));
indexCFString =
CFStringCreateWithCString(NULL, indexStr,
CFStringGetSystemEncoding());
// stuff the string into the ItemDataRef
ThrowIfOSErr(
SetDataBrowserItemDataText(
outItemData, indexCFString));
}
return true;
case kCompletedColumn:
{
UInt16 completionState;
ThemeButtonValue checkboxValue;
completionState =
this->ResolveItemID(inWhichItem)
->GetCompletion());
// check completed columns and mark partially completed
// columns using the mixed state
if ( completionState == 0 )
checkboxValue = kThemeButtonOff;
else if ( completionState >= 100 )
checkboxValue = kThemeButtonOn;
else
checkboxValue = kThemeButtonMixed;
ThrowIfOSErr(
SetDataBrowserItemDataButtonValue(
outItemData, checkboxValue));
return true;
}
case kPercentageColumn:
// percentage is [0..100]
ThrowIfOSErr(
SetDataBrowserItemDataMaximum(
outItemData, 100));
ThrowIfOSErr(
SetDataBrowserItemDataMinimum(
outItemData, 0));
// completion is already returned as percentage,
// so we can just use that value
ThrowIfOSErr(
SetDataBrowserItemDataValue(
outItemData,
this->ResolveItemID(inWhichItem)
->GetCompletion()));
return true;
case kLastModifiedColumn:
ThrowIfOSErr(
SetDataBrowserItemDataDateTime(
outItemData,
this->ResolveItemID(inWhichItem)
->modificationDate));
break;
}
return false;
}
ToDoListWindow::OnSetItemData()
Store modifications made by the user back to our ToDoItem objects
bool ToDoListWindow::OnSetItemData(
DataBrowserItemID inWhichItem,
DataBrowserPropertyID inWhichProperty,
DataBrowserItemDataRef inOutItemData)
{
switch ( inWhichProperty )
{
case kTitleColumn:
{
CFStringRef newTitle;
ToDoItem* item(this->
ResolveItemID(inWhichItem));
// retrieve the modified string
ThrowIfOSErr(
GetDataBrowserItemDataText(
inOutItemData, &newTitle));
// make sure the user actually made some modifications
if ( CFStringCompare(
newTitle, item->title, 0)
!= kCFCompareEqualTo )
{
item->SetTitle(newTitle);
GetDateTime(&item->modificationDate);
// make sure the ‘last modified' column also gets an update,
// since we adjusted the item's last modification date
//
if ( item->parent )
ThrowIfOSErr(
UpdateDataBrowserItems(
mBrowser,
item->parent->id,
1, // numItems
&item->id,
kDataBrowserItemNoProperty,
kLastModifiedColumn));
// if our sort property is either the title or the
// modification date column, we'll have to trigger a re-sort
// of the databrowser, since UpdateDataBrowserItems
// does not trigger this automatically.
//
DataBrowserPropertyID sortProperty;
ThrowIfOSErr(
GetDataBrowserSortProperty(
mBrowser, &sortProperty));
if ( sortProperty == kLastModifiedColumn
|| sortProperty == kTitleColumn )
ThrowIfOSErr(
SortDataBrowserContainer(
mBrowser, kRootItem, true));
}
CFRelease(newTitle);
return true;
}
case kCompletedColumn:
{
ThemeButtonValue checkboxValue;
ThrowIfOSErr(
GetDataBrowserItemDataButtonValue(
inOutItemData, &checkboxValue));
ToDoItem* item(this->
ResolveItemID(inWhichItem));
if ( item->childItems.empty() )
{
// for leaf items, the possible completion states are
// either done or not, i.e. 0 or 100 %
//
item->SetCompletion(
checkboxValue == kThemeButtonOn ?
100 : 0);
// Update our parent container chain's completed and
// percentage column properties, so they'll reflect
// the modified state
//
ThrowIfOSErr(
UpdateDataBrowserItems(mBrowser,
item->parent->id,
1, // numItems
&item->id,
kDataBrowserItemNoProperty,
kPercentageColumn));
for ( ToDoItem* container = item->parent;
container != NULL &&
container->id != kRootItem;
container = container->parent )
{
ThrowIfOSErr(
UpdateDataBrowserItems(mBrowser,
container->parent->id,
1, // numItems
&container->id,
kDataBrowserItemNoProperty,
kCompletedColumn));
ThrowIfOSErr(
UpdateDataBrowserItems(mBrowser,
container->parent->id,
1, // numItems
&container->id,
kDataBrowserItemNoProperty,
kPercentageColumn));
}
// if our sort property is either the checkbox or
// the progress bar column we'll have to trigger
// a re-sort of the databrowser, since UpdateDataBrowserItems
// does not trigger this.
//
DataBrowserPropertyID sortProperty;
ThrowIfOSErr(
GetDataBrowserSortProperty(mBrowser,
&sortProperty));
if ( sortProperty == kCompletedColumn ||
sortProperty == kPercentageColumn )
ThrowIfOSErr(
SortDataBrowserContainer(
mBrowser, kRootItem, true));
}
else
{
// don't allow the checkbox for container items to be
// modified by the user. This is done by just returning
// false from this call, which will be translated into
// the correct error code by our entry point member.
return false;
}
return true;
}
}
return false;
}
There's not much to say about the main entry point function, except that it returns a special error code errDataBrowserPropertyNotSupported for properties that we don't supply (i.e. if OnGetItemData or OnSetItemData return false).
In the OnGetItemData member function, we first handle three predefined properties: the "is editable" property, telling databrowser that all of our items are editable, the "is container" property, where we decide whether an item is a container based on whether our internal item representation has child items or not, and finally the property named kDataBrowserItemSelfIdentityProperty. This one is requested in column view mode for the regular display. Column view mode has a fixed display style of icon+text except for the preview area, which is a custom content type. So, when in column view mode, we'll display an icon in addition to our item's title. Since I was too lazy to design some icons, I chose to use the clipboard item for plain task items and a folder icon for task groups (i.e. container items). The code then continues to the kTitleColumn property, which will stuff the column title into the DataBrowserItemDataRef.
The DataBrowserItemDataRef type is a reference to an opaque container that is used to transport the data to and from the databrowser. There are a number of different setter and getter functions that allow you to store and retrieve various types of data (e.g. Get/SetDataBrowserItemDataText for CFStringRef). Usually, databrowser only accepts one specific data type for a given property (e.g. a CFStringRef for text properties), with the exception of the icon+text column type, which of course allows you to specify both an IconRef and a CFStringRef. Also, for progress bars and relevance ranking bars you have to specify a minimum and a maximum value in addition to the actual value.
When looking at the code for ToDoListWindow::OnSetItemData, you'll notice quite a few calls to UpdateDataBrowserItems. This is because some of our properties are actually related: the checkbox column is just a Boolean (well, not quite, it may have three states) way of displaying the information that is also presented in the progress bar column, namely the progress of the task. So when a checkbox is toggled, we also have to update the progress bar and, of course, since the containers' progress is calculated by summing up the progress of their child items, we also have to update all the enclosing containers' checkbox and progress bar properties.
This calculated progress value for containers is also the reason why we don't allow the user to toggle the checkbox for a container item. By returning false from this member, our entry point will return errDataBrowserPropertyNotSupported to databrowser, which indicates that we didn't allow modification of this particular piece of data. Databrowser will then again use the itemData callback (in it's getter variant) to query the correct data for this property and redraw the cell.
The other thing to note about this function is that when a checkbox value is changed, we'll trigger a re-sorting of the databrowser. Databrowser by itself does not change the sort order when the user edits values in the sort column (at least not in the current implementation). Depending on how you are using databrowser, rearranging items immediately might not be what you expect, so this is left to be done manually. In our case, we want to have completed tasks to be sorted properly even after editing, but you'll also notice that this may be rather disturbing as they move immediately after you toggle a checkbox, making it impossible to toggle this checkbox again without starting a search for your item. While this may be feasible for a to-do list, in general this behavior may confuse the user.
Notification callback
Next, let's have a look at the item notification callback. The sample code only handles the single notification required for hierarchical databrowsers: the "container opened" notification. Whenever this notification is sent, the items inside the container have to be added to the databrowser. Since our ToDoItem object contains a list of all its child items, this is fairly easy.
The last parameter to this callback, the DataBrowserItemDataRef, is not currently being used, but allows the flexibility to pass more detailed information about some notifications in the future.
Listing 4 shows the item notification callback (of course, there is also a static member function serving as the main entry point for the callback - just like with the item data callback, which just dispatches to this regular member function for convenience):
Listing 4: Item notification callback
ToDoListWindow::OnItemNotification
Callback for notifications from databrowser. Handles only a single notification: "container opened"
in response to which we'll fill the container with items
void ToDoListWindow::OnItemNotification(
DataBrowserItemID inItemID,
DataBrowserItemNotification inMessage,
DataBrowserItemDataRef inOutItemData)
{
switch ( inMessage )
{
case kDataBrowserContainerOpened:
{
// a container was opened -> add all the child items
//
ToDoItem* container(
this->ResolveItemID(inItemID));
if ( !container->childItems.empty() )
{
std::vector
childItemIDs(
container->childItems.size());
// convert from ToDoItem object pointers
// to DataBrowserItemIDs
std::transform(
container->childItems.begin(),
container->childItems.end(),
childItemIDs.begin(),
GetItemID);
// add the items to databrowser and specify
// that they are presorted by their index
ThrowIfOSErr(
AddDataBrowserItems(
mBrowser,
inItemID,
childItemIDs.size(),
&childItemIDs[0],
kIndexColumn));
}
}
break;
}
}
This is our first encounter with the API used to add items to a databrowser: AddDataBrowserItems. This function allows you to add multiple items simultaneously. Wherever possible you should take advantage of this feature, as it allows databrowser to minimize the overhead for invalidating and updating.
Also, the last argument is a property ID that tells databrowser how the items are sorted within the array you are passing. If this sorting matches one of your column sort orders, pass that column's property ID (in our case, it matches the index column's sort order). If the databrowser is currently sorted by that column, databrowser can use a slightly more efficient insertion algorithm. This doesn't really matter in our example, since the number of items we add is usually not very large, but once you add a few thousand items, this may well make a difference. If the items aren't sorted by any specific column property, just pass kDataBrowserItemNoProperty.
Keeping order
So, how does databrowser compare items? Fortunately, the answer is: it's up to you! Consider, for example, our index column: it is a text column where we pass a string representation of the item's index to databrowser. Now, we all know that comparing strings that represent numbers doesn't quite cut it: you'd usually get orders like "1", "10", "2",... so if databrowser would just sort by comparing the strings we pass, this would produce a wrong result.
However, since we know the number representation for the index, it's quite easy for us to perform a real number comparison.
For that reason, databrowser uses the item compare callback to let you compare two item properties. Well, I probably should be a little more exact here: your callback determines the order relation between any two items. Databrowser just passes the current sort property ID as a hint on what you should compare. However, it's totally up to you to define the order relation between the items, so if you chose to compare a different property, databrowser will happily display the items in that order.
Of course, in most cases, this would probably confuse the user, since they usually expect the items to be ordered by their visual representation when they chose a column to sort by. But it's just important to note that the property ID passed is just a hint as what you compare, but databrowser does not rely in any way on the fact that you have to compare using exactly this property.
Another fact I should probably mention is that the sort algorithm used by databrowser is a stable sort algorithm, which means that during sorting, if any two items are identical, they won't change their relative order. This allows the user to apply multiple sort criteria by sorting several times in reverse order of significance (applying the primary sort property last), which partially makes up for a shortcoming of databrowser, namely the fact that it only knows about a single sort property instead of primary, secondary, etc. sort criteria.
Well, let's have a look at ToDoListWindow's implementation of the item comparison callback (Listing 5). The comparison callback should return a Boolean value, which indicates whether the first item is to be considered "less than" the second item.
Listing 5: Item comparison callback
ToDoListWindow::CompareItems
Callback from databrowser to compare two items. Compares using the native representation of the data.
Returns whether item 1 is "less than" item 2.
bool ToDoListWindow::CompareItems(
DataBrowserItemID inItemOne,
DataBrowserItemID inItemTwo,
DataBrowserPropertyID inSortProperty)
{
ToDoItem* itemOne(
this->ResolveItemID(inItemOne));
ToDoItem* itemTwo(
this->ResolveItemID(inItemTwo));
switch ( inSortProperty )
{
case kIndexColumn:
return GetItemIndex(itemOne) <
GetItemIndex(itemTwo);
case kTitleColumn:
return CFStringCompare(itemOne->title,
itemTwo->title,
kCFCompareCaseInsensitive) ==
kCFCompareLessThan;
case kCompletedColumn:
case kPercentageColumn:
return itemOne->GetCompletion() <
itemTwo->GetCompletion();
case kLastModifiedColumn:
return itemOne->modificationDate <
itemTwo->modificationDate;
default:
return inItemOne < inItemTwo; // just *something*
}
}
That's it?
So, these were the "required" callbacks that enable databrowser display the items correctly. Of course, there are more callbacks one can supply, but I won't be able to discuss them all in this article. The sample application implements a couple more callbacks to support drag & drop and help tags. Take a look at the source code for ToDoListWindow (in file ToDoListWindow.cpp) to see their implementations.
If you want to support column style browsers in your application, you probably also want to implement at least the custom draw callback, so you can draw something into the preview area when a leaf item is selected. The example app also implements this callback and draws the untruncated title into the preview area.
Gotchas & Tips
Having shown all this, I'd like to warn you about some of the pitfalls you might hit on your way:
First, you should keep in mind that databrowser is a very complex control - it even automatically embeds other controls (e.g. the scrollbars) that you don't know about. This might not affect you, but for some frameworks it might come as a surprise seeing a control they never added. Also, since the scrollbars are embedded inside databrowser, you might have a hard time passing events to the correct control if your own control hierarchy is not a true embedding hierarchy but rather managed by your own framework.
Make sure you have a root control in your Window: inline editing in databrowser's text columns won't work unless the window has a root control. On Mac OS X, a root control is created for you automatically, but on Mac OS 9, you'll have to create the root control yourself using the CreateRootControl API.
Another pitfall is that databrowser is the first control to rely on the documented requirement that a GrafPort's origin must be {0,0} when calling ControlManager APIs. You'll see all kinds of weird offset problems if databrowser is called with the origin set to something else using SetOrigin. Unfortunately, that's what the current PowerPlant implementation does to provide a local coordinate system for each pane. However, at the time of this writing, it seems like Metrowerks in working on a version of PowerPlant that gets rid of this limitation.
When switching between list view and column view style, you will have to re-configure your databrowser when switching back to list view style, i.e. you have to add all your column definitions again, since once you enter column view style, databrowser forgets about the column configuration. Column view style cannot be configured, so switching to column view style works without any extra work, but when switching back, your previous list view column setup is lost (the sample code handles this, so you can have a look there).
If you allow your users to toggle back and forth between list view and column view mode, it might also be a good idea to use the Get/SetDataBrowserUserState API to save and restore the user state as you leave resp. enter list view mode. Beware though, that SetDataBrowserUserState will only work after you have added all the columns that have been in there before - it won't restore the columns themselves, just the user modifiable settings of these columns.
If you are using databrowser in a dialog and the databrowser items do not allow user interaction while in background (i.e. no drag & drop), it's probably a good idea to have the databrowser content dimmed when the control is inactive. By default, databrowser will only dim the header buttons, but not the content. You can achieve content dimming by calling SetDataBrowserActiveItems with the appropriate Boolean parameter. When passing false for the "active" parameter, all items will be considered inactive, regardless of their kDataBrowserItemIsActive property. After calling this API with true for the "active" parameter, the active state for each item will be restored to what the item's kDataBrowserItemIsActive property specifies.
Remember what I said earlier about consistent notifications? You'll get into trouble when disposing your callback UPPs (or, if you use an object like in the sample code, disposing the object) before the databrowser control is disposed, since databrowser will attempt to send "item removed" notifications as it is disposed. To play safe, you may want to call SetDataBrowserCallbacks with a NULL parameter for the callbacks to reset them back, so databrowser won't try to access your UPPs.
Be careful when specifying custom fonts, styles and sizes for the column headers: Keep in mind that databrowser on X has a fixed header button height that is given by the OS, so if your font settings increase the pixel size of the text, it might get cut off.
To specify custom font style settings for the databrowser contents, use the regular SetControlFontStyle API. You may need to adjust the databrowser row height to accommodate larger pixel sizes of the characters.
If you plan to incorporate databrowser in your Mac OS 8/9 product, you should be aware that - at the time of this writing - there is no complete API parity yet between databrowser in CarbonLib 1.3 and databrowser in Mac OS X 10.0. Apple plans to bring the APIs on par in a later release of CarbonLib, so you may want to require that release (once it becomes available) if you want to create a single binary running on Mac OS 9 and Mac OS X.
Conclusion
Databrowser is a very powerful tool to display your data. It has been designed to be able to accommodate large amounts of data, but it's also easy enough to manage, so it can be used even for small, flat lists of simple data. This allows you to use databrowser as the display front end for any kind of list data your application needs to display.
Whether used to browse a file system like hierarchy, or as a front-end to your database, or even for a plain list à la ListManager, databrowser proves to be a scalable solution for many of these purposes.
I did some basic performance tests and found that databrowser has no problems whatsoever to handle 100,000 items (although it might be another question whether it makes sense to even allow the user to browse through 100,000 items).
Memory footprint should be relatively small (about 20 bytes per item in total in the current implementation), so databrowser is a really scalable solution for applications that need to present data in a tabular form to their users.
As mentioned in the beginning, using databrowser also saves you from the hassle of playing catch-up with Finder's list and column views when trying to provide a consistent UI experience to your customers.
As we saw in the discussion of the sample code, only a few API calls are required to get your databrowser configured. By relying on the built-in display types, we could leave out any rendering or formatting code altogether (O.K., except for the index column, for which we created a formatted string representing the index' numeric value). Most of the code discussed was just item data exchange with databrowser. The ability to store your data in whatever seems the appropriate way (in our case objects of class ToDoItem) enables you to write much more readable code as you don't have to write code that will access databrowser's internal data, potentially having to use heavy casting.
Starting from either the sample code for this article which is available by FTP or from the BasicDataBrowser sample code that comes with the Carbon SDK, you should be able to start test driving databrowser and see how it fits into your application.
Where to Go From Here
Make sure you take a look at the DataBrowser Technote TN 2009, available at http://developer.apple.com/technotes/tn/tn2009.html. It contains valuable information about the databrowser APIs.
Also, check out the sample code section of the CarbonSDK: there's a "BasicDataBrowser" sample which also shows how to create a simple browser (although some of the features in the sample aren't yet wired up as of this writing) as well as an implementation of a LDataBrowser PowerPlant class.
Another valuable source of information for me has been the Carbon Development Mailing List
http://lists.apple.com/mailman/listinfo/carbon-development. Quite a few developers on this mailing list have started to use databrowser and are willing to help others with their problems.
Documentation on the Carbon Event Manager (and CarbonEvents), which is used exclusively to drive the sample app, is available at
http://developer.apple.com/techpubs/macosx/Carbon/oss/CarbonEventManager/carboneventmanager.html.
Special thanks to Jim Rodden for reviewing this article and answering many of my databrowser related questions on the Carbon Development mailing list. Also, thanks to Martin Bestmann for reviewing the article.
Jens Miltner jum@mac.com works for Netopia where he's responsible for the design of netOctopus' cross-platform application framework as well as the framework's Macintosh implementation. When he's not twisting bits & bytes, you can probably find him chasing through the house with his two little sons or playing basketball.