|
Kylix for Delphi programmers |
|||
Kylix is very compatible with Delphi, but only the simplest programs can be moved between the two without some amount of conversion. There are three major sources of incompatibility. Kylix GUI programming is different from Delphi GUI programming because where Delphi's VCL is based on the Windows GDI, Kylix's CLX is based on Trolltech's Qt library. While the two frameworks are very similar, they are not identical. GUI programming is also different between Windows and Linux because the Windows desktop architecture is quite different from Linux's X11-based desktop architecture. While Qt and CLX do a good job of masking those differences, they do cause a few of the most annoying (because least tractable) differences between a GUI program running on Windows and the 'equivalent' GUI program running on Linux. Finally, Linux is ultimately a different operating system than Windows. Overall, the process model is quite similar to Windows, but the API's are quite different. You might not notice much difference if all your code uses Delphi's TThread and Delphi's synchronization objects (events and critical sections.) However, if you've been routinely dipping down to the Windows API for thread and process control, you'll have to rewrite all that code. You'll also find that there are some things that you can do trivially under Windows like wait on multiple events, or wait for file/directory changes that will have to be drastically rewritten for Linux. About this paperThese three sources of incompatibility generate a wide variety of porting problems. I start with desktop programming - differences in the Kylix and Delphi visual libraries and then I move on to the ways that the differences between Linux and Windows affect non-visual library code. Because this paper is basically a very slightly extended version of my talk, I stay very focused on the most important differences. What's more, I don't cover any of the issues in any great depth; I really just sketch where each issue comes from and how it affects you. Fortunately, Google - and especially Google Groups have made it very easy to find the answers to all sorts of questions. I've also included three sidebars and a couple of appendices that contain extra, more specialized information. And, of course, you can always buy my book, which has both a detailed table of contents and a rather nice index, and which covers everything in the sort of depth that a four or five thousand word paper can't. Desktop programmingBecause this paper parallels my talk and I expect that I have more material than time, I've ordered these sections so that I cover more common situations before I cover less common situations. That minimizes the impact on my audience if I run slow and have to skip to the next section. That's why I start the desktop programming section with the loathsome "window manager" issues. They're the first things you'll see (once your code compiles and runs!) and there's so little you can do about them. Window manager issues aren't problems to fix; they're problems to avoid. Window manager issuesThis is something that will seem very weird to Windows programmers, but on Linux many key user interface "policy" decisions are left to plug-in "window managers." Appendix 1 covers the X11 architecture in a bit more detail, but all that you really need to know is that there are a lot of different window managers available under Linux, and they are often chosen more on the basis of eye-appeal than standards compliance. Some window managers are common because they're the default with Gnome or KDE on popular distributions, but every user can change their window manager. What this means to you as a programmer is that key aspects of form behavior - like the form's resize and maximize behavior, the border appearance, and the presence or absence of system buttons - varies from system to system. Borland can't fix this: they emit standards-compliant "hints," but they have no control over how or whether the window manager takes the hint. Similarly, unless you work in a vertical market shop and have complete control over the configuration of the boxes you sell, you just have to accept that the behavior you see is not necessarily the same behavior that your users see. In general, there's no way to know whether the window manager follows your wishes or not. BordersForms have the same BorderIcons and BorderStyle properties under Kylix as under Delphi. However, they don't act the same way. Some window managers will insist on giving each window a minimize, maximize, and close button, whether you want one or not. There's nothing you can do about this except, perhaps, document the window managers that you know act well. Some users especially those using a non-compliant window manager only because it was standard with their Linux distribution, or because it looked pretty - will be willing to try a different window manager. However, most will not change window managers, either from simple inertia or because they really like the one they have now; for the most part, bad BorderIcons behavior is just something you'll have to live with. Somewhat similarly, some window managers won't draw, say, a bsSizeable BorderStyle any differently than they draw a bsSingle or a bsDialog BorderStyle. There's not much you can do about this so don't construct a user interface that expects users to notice differences in the border style. (Of course, this is a good idea even when the differences are real.) However, it's not just that a bsSizeable BorderStyle might look like a bsSingle or a bsDialog BorderStyle - a bsSizeable BorderStyle might act like a bsSingle or a bsDialog BorderStyle. That is, there are window managers that think all windows are resizable! So, don't expect setting BorderStyle to bsDialog or bsSingle means that a Kylix form can't be resized at runtime. Fortunately, the Constraints property works just fine on Kylix. If you want to have a form that a user can't resize at runtime, give the form an OnShow handler that sets Constraints.MinWidth and Constraints.MaxWidth to the form's Width and both the MinHeight and MaxHeight to the Height. (If you put a form that can do this in the Object Repository, all your projects that need a fixed-size form can inherit from it.) Note that you should set the Constraints at runtime, and not design time, as different window managers will set borders of different widths. If a user's window manager uses a wider or narrower border than yours does, setting form Constraints at design time can make for a form whose bottom and right sides are cut off, or a form with unexpected white space on the bottom and right sides. MaximizedOn Windows, a maximized form gets the whole "work area", which means that it always leaves the Windows taskbar alone. You can get a true full-screen window, but you have to ask for it specially. Full-screen is not the same as maximized, on Windows. On Linux, though, sometimes it is and sometimes it isn't. That is, some window managers give your form the whole screen when it's maximized and some don't. Worse, you can't reliably know which behavior you got, because the WindowState will not necessarily be wsMaximized when the window is maximized. You can get an OnResize event that claims you have a wsNormal WindowState, even when a form is maximized. However, since there's no way to get the size of the work area, you can't really know if a given large window is just a large window or is really a maximized window. Additionally, some window managers will not just get the WindowState wrong, they won't report the Top and Left of a maximized form properly either. This is annoying, if you want to use a different layout for maximized forms than for framed forms. This also means that you can't reliably persist form locations; if you restore the BoundsRect of a window that was closed maximized, you can get a large window, partially off-screen. Application Title and IconDelphi programmers are used to being able to set an application title and icon that's not the same as the main form's caption and icon. This may or may not work on Linux. Some window managers will insist on showing a panel (Linux taskbar) button for every form, and/or using the main form's caption and icon on the panel. Borderless formsKylix forms can have form-wide keyboard events by setting KeyPreview to True, just as Delphi forms can. However, Qt thinks that borderless windows are not interactive, and so don't get the keyboard focus. That means that KeyPreview doesn't work on fbsNone windows. In Kylix 1 or 2, you can force KeyPreview to work on a borderless form by including the line QWidget_setActiveWindow(Handle);in the form's OnShow handler. This may work just fine, or it may cause some side-effects. For example, under some circumstances my dropdown date picker component gets phantom mouse clicks that it has to ignore. In Kylix 3, you have to use QWidget_grabKeyboard(Handle); // ... QWidget_releaseKeyboard(Handle);and most of the techniques I use in my dropdown date picker component don't work. This lack of portability - and the possibility that future versions may be different in still other ways - means that it's a good idea to use KeyPreview on borderless forms as little as possible. Differences between CLX and VCLThere are lots and lots of little differences between the Windows VCL and the Kylix CLX. For example, there are no virtual list boxes under Kylix, and there is a separate TListView and TIconView, instead of a single TListView component that can handle icons as well as reports. Some of you will be horribly impacted by these differences; others won't even notice. What I include here represents my best guess at the differences that will affect the most people. Key NamesAny OnKeyDown or OnKeyUp event handlers that uses VK_x key names will have to be rewritten to use the new KEY_x names from the Qt unit. (It should be possible to write a unit that defines the Windows style VK_x names in terms of the Qt names, but I haven't done so, and a quick Google search (in mid-September, 2002) comes up blank.) If you need to refer to key names, you'll quickly find that Kylix forms do not automatically use Qt the way Delphi forms automatically use Windows you'll have to manually add Qt to the form unit's uses clause. "Form in form" code doesn't workA rather common "advanced" Delphi technique involves including one form as part of another by setting the embedded form's Parent to a container component on the 'master' form, such as a tab sheet, a panel, or a group box. This can make for very flexible applications that configure themselves at runtime, or that bring up any one of a wide variety of object editors in a single area. This technique does not work at all under Kylix; you'll have to change any "embedded forms" to be frames. Fortunately, frames are enough like forms that this change should be very straightforward. Bitmaps are implemented differentlyIn many ways, the CLX TBitmap type is just like the VCL TBitmap type. You may not even notice the differences if you just load an image from a file or you use simple TCanvas operations to draw an image at runtime. However, any applications that do low-level bitmap manipulation will run into a few differences. One relatively minor difference is that transparency is implemented differently than on Windows. On Windows, you can refer to the MaskHandle of a transparent bitmap; on Qt, there is no MaskHandle. Transparency works just fine, you just don't have easy access to the mask. More significantly, the CLX TBitmap has no HandleType, which allows you to switch between fast, device-dependent bitmaps [DDB], and editable device-independent bitmaps [DIB]. Instead, each bitmap exists simultaneously as a Image, which corresponds more or less to a DIB, and a Pixmap, which corresponds more or less to a DDB. The Image is what gets loaded and saved, and the Pixmap is what gets drawn to the screen. Kylix keeps the two in synch, and you rarely have to pay attention to the implementation details. One place where you do, though, is when you are working with the ScanLine property. The ScanLine allows you to read and write the Image side of the bitmap. Before a changed image can be displayed, any changes have to be reflected on the Pixmap side. Unfortunately, there's no way to tell Qt 'I just changed this rectangular region of this Image; please migrate those changes to this Pixmap' you have to regenerate the whole Pixmap. The ScanLine property's read function - QGraphics.TBitmap.GetScanLine - handles this by calling FreePixmap before returning a pointer to the first pixel. This means that the next time you Draw the bitmap or do anything else that requires a Pixmap, Kylix will automatically regenerate one. This works just fine if you build the whole bitmap before you Draw it or assign it to a Brush. However, if you are doing some long operation on a large bitmap and wish to show your progress, line by line, you have to pay attention to this issue. Each time you get the ScanLine of a new row, Kylix calls FreePixmap. If you haven't done anything that required a QPixmap since the last read of the ScanLine property, this has no real effect. However, if you've gone and done a CopyRect of the updated line to the screen, you've created a new Pixmap. This took a while, which means your line-by-line processing is limited by the need to keep recreating (and freeing) Pixmaps of the whole image. The best solution is to maintain a one row working bitmap, and manipulate its ScanLine[0], then CopyRect it to the large bitmap and to the screen. Remember, you'll need to FreePixmap at the start of each row, either by reading ScanLine[0] again or by explicitly calling FreePixmap. Other differences
Pretty Qt stuffIn many way, Qt is a much nicer environment than Windows. The object API is a lot more sensible than the thousands of flat functions, grouped by history or role and then alphabetized. Qt also includes some of the feature's in XP's GDI+ - if a pretty, shiny interface helps your users love your program, you'll love Qt. Bitmapped backgroundsMost VCL components let you set their background Color. CLX components act the same way, but they also let you set a background Bitmap. (A component's Color only matters if it has no Bitmap.) There is no ParentBitmap property. The background Bitmap is drawn tiled behind the component's visuals; when a component shares a background Bitmap with its container, the backgrounds on the 'contained' components (like radio buttons on a panel) are aligned with the backgrounds on the container so that it looks like a single background. (That is, the top-left pixel of each contained component's background is not necessarily the top-left pixel of the background Bitmap.) New border stylesComponents have more choices in BorderStyle's than bsSingle and bsNone. More components like labels - can be bordered than under the VCL. Many forms that used panels or bevels to frame some text will no longer need to. Extended text handlingTCanvas's TextRect() and TextExtent() properties take an optional TextFlags parameter. This allows you to control horizontal and vertical alignment, word wrapping, tab expansion, and ampersand to underscore conversion. The OS is the foundationWhere most of the "port stoppers" in the previous section came from the way Qt and X11 differ from Windows, most of these differences are more fundamental to Linux, and Unix in general. No "application directory"Windows programmers are used to placing configuration file and app-private libraries (and packages) in the same directory as the application executable. Neither technique works on Linux. The Linux File Model appendix (Appendix 2) explains why, but basically you can't use ParamStr(0) or Application.ExeName to find a global configuration file. You have to place configuration in globally privileged places like /etc. If another program wants to use the same /etc filename? You're hosed. Somewhat similarly, you can't prevent DLL Hell by placing copies of known-compatible libraries in the same directory as the executable, and having them shadow any less-compatible libraries that may be on the system. Linux uses a better-than-nothing system of symlinks to avoid DLL Hell, but it's hard to do a zero-impact install. That's because Linux will only look in a few privileged directories, and in the directories named in your LD_LIBRARY_PATH environment string. (For more information, see Sidebar 1 "Linux library load order.") This causes issues even deploying Kylix GUI apps to machines that don't have Kylix installed and complicates deployment of apps that use packages. What you want is a script that is "branded" with the knowledge of where your app and any libraries (which includes packages) were installed. When users run the script, it sets a local copy of the LD_LIBRARY_PATH environment variable, so that your app runs without affecting the calling environment. This is straightforward, by bash script standards, but definitely out of scope here. Standard outputIf you know anything at all about the old Turbo Pascal Write() and WriteLn() procedures, you probably know that you can't use them from a Delphi GUI app. If you do use them, you'll get an exception. What you may not know is that you can place the same {$APPTYPE CONSOLE} pragma in the project file of a GUI app as in a console app. This pragma causes the runtime code to automatically call the Win32 API functions that create a console window when the application runs. A "console application" without this {$APPTYPE CONSOLE} pragma is a 'daemon' with no desktop presence; a GUI application with the {$APPTYPE CONSOLE} is a GUI application that also has a console window. A GUI application with a console window can use Write() and WriteLn() without generating any exceptions. This can be useful for debugging. All Kylix applications whether GUI or console, whether they contain an {$APPTYPE CONSOLE} pragma or not can write to the Linux standard output and standard error devices at any time. (The point of having separate output and error devices is that you can handle the normal output separately from the error output. If you pipe the output of one command to another, the error messages will still go to the console. Similarly, you can redirect the normal output to one file and the error output to another file: foo > normal.output 2> error.output.) You can write to standard output by calling WriteLn(Output, 'Hello, World.');or (more normally) just WriteLn('Hello, World');Similarly, you can write to the standard error device with WriteLn(ErrOutput, 'Something''s wrong, again!'); Standard output in the IDEBy default, a Linux process inherits its parent process's standard input and output devices. This means that a WriteLn() from a program running in the IDE will go to the console window that you typed startkylix & in, if any. If that window is visible under the Kylix windows, you'll see your program's trrace statements. If it's not visible or you started Kylix from one of the start menu entries that the installer created you won't see any WriteLn() text. However, if you bring up the Run / Parameters dialog and select "Use Launcher Application", you'll get a new console window where your trace statements will be visible until the program terminates. You can even ReadLn() from this window, but you generally won't want to. One exception is that putting a ReadLn; at the end of the project source keeps your program running and the console open - until you hit return. Probably a better way to do this is to just add the hold option to the Launcher Application command string. MemoryThe default Kylix implementation of GetMem is a straightforward call to Libc.malloc(), which does "suballocation". That is, when you ask for a small block (like 1K), it will allocate a much larger block (like 64K) and carve the small block out of it. This minimizes fragmentation of the system heap, but also means that you will not necessarily get a SIGSEGV if you dereference a pointer after you free it. For example, figures 1 and 2 show a test project that deliberately dereferences freed memory.
procedure TSuballocFrm.OneKBtnClick(Sender: TObject); var P: PInteger; begin ClearLog; GetMem(P, 1024); LogLn('Allocated 1024 bytes'); FreeMem(P); LogLn('Freed 1024 bytes'); try F(P^); LogLn('No exception on deref'); except on E: Exception do LogLn('Exception %s: "%s"', [E.ClassName, E.Message]); end; end; // TSuballocFrm.OneKBtnClick In Figure 1, referring to the 1K block after it was freed does not generate a SIGSEGV. However, in Figure 2, running code that's identical except in that it allocates a much larger block does generate a SIGSEGV, which Kylix reports to you as a EAccessViolation exception. GetMem (or, rather, malloc()) handled the 1K allocation by carving a chunk out of a larger block. When the 1K block was freed, it was added to a free list within that larger block, but the block as a whole stayed allocated and assigned to the Suballoc application. Thus, any pointer to any byte within that larger block remained a valid pointer that will not generate a hardware exception when dereferenced. By contrast, the 1M allocation was handled by allocating a whole new block from the system free list. The allocation created a new entry in the hardware page tables for the 1M block belonging to the Suballoc application. When the 1M block was freed, the whole thing went back on the system free list, and the page table entry went away. Dereferencing the pointer generated a hardware exception. Debugging Dynamic MemoryI don't have time to cover this, but Linux offers some tools for detecting common dynamic memory bugs like buffer overruns or freeing the same block twice. See Sidebar 2 - "Debugging Dynamic Memory" - for more information. ThreadsI could say a lot about threads. In fact, I do, in my Kylix book. All I have time for here is a few bullet points. TThread and pthreadsWhile the POSIX threads [pthreads] library that Kylix uses is quite different from the Windows thread API, Kylix's TThread itself isn't very different from Delphi's. If you just override the TThread.Execute procedure and do some simple in-Delphi synchronization, you may never notice the differences. The two key differences are:
EventsAs with threads, Borland has done a great job hiding the differences between the Windows and Linux synchronization API's. There are just three things that you really need to know.
Finally ...I don't want to give the wrong impression with all these details. Kylix is very compatible with Delphi, but there are lots of little differences. In general, the more your Delphi programs stick to the VCL and other Borland code, the less trouble you'll have moving them to Kylix; the more your code uses the Windows API, the more work you'll have porting your applications. Jon Shemitz is a consultant and the author of a popular Kylix book. Jon works in Santa Cruz, California. You can contact Jon via his web site at www.midnightbeach.com. AppendicesThe two appendices contain background material that will help you understand just what a window manager does (Appendix 1), and some of the advantages and disadvantages of the Unix system of inodes and file names (Appendix 2). |