On all platforms, external components are loaded from the relevant sub-directory (xcomps, jscomps and logcomps folders) of both the Omnis data folder and the Omnis program / Application folder.
Omnis external components are plug-in modules that extend the range of visual and non-visual objects available in the design and runtime environments in Omnis, as well extending the Omnis programming language. There are many different external components supplied with Omnis, but you can create your own using your own software development tools and the information in this manual.
Once built and installed into Omnis, external components behave in exactly the same way as standard built-in Omnis components. You can change the properties of an external component in design mode using the Property Manager. Likewise, at runtime you can manipulate an external component using methods and the notation, and examine its runtime properties in the Notation Inspector. External components can also contain functions or methods and events, which you can call or intercept using Omnis methods. You can build all of these features into your own external components.
The type and range of external components include:
Window objects (including background objects) and Report objects
Both window and report external components appear in their respective group in the Omnis Component Store and can be used in your libraries in exactly the same way as built-in GUI objects.
Static Functions
Static functions are components that contain functions, that appear in the Omnis Catalog under the ‘Functions’ group. These functions can be invoked from calculations in your Omnis code.
Omnis objects
Omnis objects or so-called ‘non-visual’ components are objects that can contain methods and properties, which can be used in the Omnis language or called to perform some specific function. External objects can be sub-classed, just like normal Omnis objects, to form new objects. The SQL DAMs are examples of non-visual components.
This manual describes how you can build external components, for adding to Window and Report classes, or for using in your code, but if you are creating components for the JavaScript Client (to add to Remote forms) you need to consult the following manual: JavaScript Component SDK
Using the libraries supplied, you can create Omnis external components that run under all platforms supported in Omnis. All of the samples supplied have independent source code. The Omnis resource compilers for Linux and macOS (Xcode) are supplied. These compile simple Windows style .RC files, and support image types .BMP, allowing the entire component to be portable.
When you start Omnis, you have to tell it to load your new component. You can load an external component via the #EXTCOMPLIBS system table. You can access this via the Studio Browser, or open a Window class in design mode and right-click on the Component Store, and select the External Components option.
If the components you create are OK, they should appear in the #EXTCOMPLIBS system table. If you cannot find your component in the external component list, check the Omnis Trace Log window. Omnis will always write any errors to the trace log during startup.
In the External Component dialog, use one of the radio button options to load the component, either ‘Starting Omnis’ or ‘Opening <libname>’. When you return to the design window, your component will be added to the relevant group in the Component Store (as defined in the component), e.g. the Other group (or you can use the Search box to find the component). You should be able to drag the control on to a window class and your component is created.
On all platforms, external components are loaded from the relevant sub-directory (xcomps, jscomps and logcomps folders) of both the Omnis data folder and the Omnis program / Application folder.
On all platforms, external components are loaded from the relevant sub-directory (PlugIns, xcomps, jscomps and logcomps folders) of both the Omnis data folder and the Omnis program / Application folder.
If there is a component (.dll, .u_... or .so, depending on platform) with the same case-sensitive name in both relevant sub-directories of the data and program folders, the component in the data folder is loaded.
If you are upgrading to the latest version of Omnis Studio (e.g. version 11), and you have created your own external components for a previous version, then these components will need to be recompiled for the new version of Omnis Studio using the latest external component source files, which can be downloaded here:
https://www.omnis.net/developers/resources/download/tools/buildyourown.jsp
Omnis supports two window types. Top level windows and child windows. In the Omnis IDE, you can create top level windows as window classes, and design the contents of the window by adding controls such as buttons and lists. All window controls such as button and list controls are child windows. A child window is a window that sits inside another parent window. Child windows can also contain other window controls, thus the parent-child relationship can be nested at several levels. For example, a scrollbox window field is a child control within the window class, but it can have other child controls placed within it, thus making it a parent.
An external component operates inside a child window and performs some kind of operation within the child window. The component can do virtually anything from draw a graph, scroll a message, or pick up a click within it and send a message back to Omnis. To do this, the window receives and processes messages. A message informs the child window of all events that affect it, such as the user clicking on it with the mouse. Later, when you create a component, you need to tell Omnis the name of a procedure that Omnis can call with your message. This procedure is often referred to as the WNDPROC (short for Window Procedure) or message handler. There are many messages defined by the component library that your procedure is sent, some you will want to deal with, others you can ignore; you will see how to deal with these messages.
To help you write platform-independent components, you should use the data types declared by the component library. All APIs in the library use the following data types.
| C-type | Omnis type | Description |
|---|---|---|
| unsigned char or unsigned long* |
qchar | standard unsigned char value *qchar is defined as 4 bytes for Unicode targets. |
| char or unsigned short |
qoschar | platform API-dependent Unicode character. 2 bytes for Win32 & Mac OSX Unicode targets. 1 byte for Linux targets & non-Unicode targets. |
| unsigned char | qbyte | assumed to hold 0-255 |
| unsigned char | qbool | assumed to hold qtrue or qfalse |
| short | qshort | standard short value |
| unsigned short | qushort | standard unsigned short value |
| long | qlong | standard long value |
| unsigned long | qulong | standard unsigned long value |
| platform dependent | qreal | used for real arithmetic |
| short | qret | return type from some API calls |
| enum | qnil | can be used to assign to some objects to clear them |
| unsigned char | qint1 | 1 byte unsigned integer (as stored on disk) |
| short | qint2 | 2 byte integer (as stored on disk) |
| unsigned short | qword2 | 2 byte unsigned integer (as stored on disk) |
| long | qint4 | 4 byte integer (as stored on disk) |
| unsigned long | qword4 | 4 byte unsigned integer (as stored on disk) |
| long | rstrno | uses when calling RESxxx functions |
| short | attnum | property numbers |
| qbool | qfalse = 0 | false boolean value |
| qbool | qtrue = 1 | true boolean value |
| qret | e_ok = 0 | no error occurred |
| qret | e_negative = 1 | error occurred |
As well as using the data types, you should try to use the component API as much as possible to ensure platform independent code. In the long run, it may mean you have to recompile for another platform, rather than having to port lots of code.
Omnis supports different types of external component which you can add to window and report classes. When Omnis starts up, the component specifies what kind of component it is, and what class type it should appear in.
cObjType_Basic
a generic window class component.
cObjType_Picture
a derived Omnis picture component for window classes.
cObjType_List
a derived Omnis list component for window classes.
cObjType_DropList
a derived Omnis droplist component for window classes.
cObjType_IconArray
a derived Omnis icon array component for window classes.
cObjType_PriOutput
a custom report output device
cRepObjType_Basic
a generic report class component.
cRepObjType_Picture
a derived Omnis picture component for report classes.
Components can be both window and report objects. For example, you may want to create a picture-handling component, that works in both window and report mode, therefore its returns type should be:
cObjType_Picture | cRepObjType_Picture
Basic Components
Basic components are generic controls that receive all messages via the WNDPROC. You must code all actions that you want to happen inside your control.
See the examples provided.
Picture Components
Picture components are objects derived from the internal Omnis picture field. Omnis calls your WNDPROC with standard messages, but you also receive some specific messages only for derived picture controls. For example, Omnis calls you to inquire how big your image is, so it can handle the scrolling and call you to paint.
See the PCX example.
List Components
List components are objects derived from the internal Omnis list field. Omnis calls your WNDPROC with standard messages, but you also receive some specific messages only for derived list controls. For example, when Omnis paints your list, you are called to draw individual lines, possibly in a selected state.
See the PICLIST example.
Droplist Components
Droplist components are objects derived from the internal Omnis droplist field. Omnis calls your WNDPROC with standard messages, but you also receive some specific messages only for derived droplist controls. For example, when Omnis paints your droplist contents, you will be called to draw individual lines.
Icon Array Components
Icon array components are objects derived from the internal Omnis icon array field. Omnis calls your WNDPROC with standard messages, but you also receive some specific messages only for derived icon array controls. For example, when Omnis paints your icon array, you will be called to draw individual icons and icon labels.
See the ICNARRAY example.
Report Components
Report components should be treated as if they were window components. When printing is required, you are called with specific report printing messages.
See the PCX example.
Background Components
Background components are objects that behave like internal Omnis background objects. For example, background objects never have the focus or receive events. They are always drawn as part of the background of the window they belong to. One of the sample background components supplied is an object that allows a bitmap to be tiled over an area.
Background components do not have their own cObjType_XXX type, and need to be defined as a cObjType_Basic type component. A flag needs to be set on ECM_CONNECT to indicate the component should be treated as a background component.
However, it is important to note that you cannot have both background and other visible components in the same library.
See the TILE or WASH example.
Omnis supports various types of non-visual components. In this context, ‘non-visual’ means a component that does not have a visual interface but one that provides some functionality, such as functions or methods, that can be used in the Omnis programming language. Most non-visual components do not need to be placed on a window or report for their functions or methods to be called. Non-visual and visual components may co-exist in the same library.
Picture Format Conversion
Picture format conversion are libraries which provide functionality to convert from the specified format to a native O/S picture and visa-versa.
See PCX example.
Static Functions
Static functions behave just like Omnis functions and appear in the catalog just like in-built Omnis functions.
See the FILEOPS example.
Object Components
Object components appear in Omnis as objects and can therefore be utilized by adding an ‘Object’ variable (with the appropriate sub-type).
Just like Omnis object classes, external object components may be sub-classed to form new objects.
See the FILEOPS example.
DAMs
Writing custom Data Access Modules for Omnis Studio is discussed in the "Omnis Studio DAM API" chapter. This chapter discusses the additional damlib library needed to build these specialised non-visual components as well as datatypes, structures, classes and general techniques involved.
See the GENERIC DAM example.
Where several instances of your component may be in use at the same time, you will need to design your code with thread isolation in mind. Use of object-oriented techniques provides the basis for thread-safety as this gives each object instance its own memory and variables.
Where shared memory or commonality exists between multiple instances, the C/C++ programming language facilitates thread management, semaphores and mutual-exclusive execution (MUTEXs) which can be used to control access to the shared resources. Any such commonality should be identified at the design stage.
You can also enhance the thread-safety of your component by passing the EXT_FLAG_SESSION flag to Omnis when processing the ECM_CONNECT message. (See Structures, Messages and Functions for more details).
Microsoft Windows
Microsoft Visual Studio
macOS
Xcode
Linux
CMake
GNU C and C++ standard libraries
Clang
Check the Readme.txt accompanying the SDK downloads for the current recommended versions of these tools.
There is one SDK source tree for every supported platform. Each source tree contains example external components, ranging from the very simple, GENERIC (lets you create a basic shell component), to the more complex controls, such as CALENDAR. The source code for the components is generally 100% cross platform and has been duplicated in the various source trees. A few components have some code which is not shared by all platforms. Such platform dependent source will usually be found in source files with names that start with an ‘x’. The source trees have scripts or projects that can be used to build the components.
When creating external components, try to keep to the tree structure, that is, if you want to create a new component called ‘Simple’, create a directory called Simple inside the source tree. Keeping to the structure will help when porting to other platforms.
One of the many samples supplied to help you create Omnis external components is generic. There are four versions of this control in the source tree that explain how to write Omnis components and give you a useful starting framework for building your own components. Before you begin to write some code, you need to setup your development environment.
Inside the SDK tree the Studio component library and header files used for building a component are located in the following folders.
Windows:
libs - contains Debug and Release versions of the component library ( complib.lib ) for linking against the multi-threaded DLL C runtime library ( dllrt ) or the statically linked multi-threaded C runtime library ( mt ).
The Studio xcomp projects use the multi-threaded DLL C runtime and locate a copy of the DLLRT complib in either the Debug_x86/x64 or Release_x86/x64 folder.
include - contains the component library header files to be used with the component library.
Use either the x86/x64 version of the Windows SDK when targeting 32/64 bit versions of Windows.
macOS:
complib/complib_debug - contain the Release/Debug component library framework ( complib.framework ).
Linux:
complib - contains the Release version of the component library ( libhomnis.a ) and the component library headers.
complib_debug - contains the Debug version of the component library.
Windows 32 bit:
Select the project file at:
\Windows-SDK-11-x86\xcomp\generic\windows\generic.vcxproj
and open with Visual Studio.
Choose either the Debug or Release Solution Configuration and build that solution.
The DLL generated will be placed in one of:
Windows-SDK-11-x86\build\Release_x86\xcomp\generic.dll
Windows-SDK-11-x86\build\Debug_x86\xcomp\generic.dll
Windows 64 bit:
Select the project file at:
\Windows-SDK-11-x64\xcomp\generic\windows\generic.vcxproj
and open with Visual Studio.
Choose either the Debug or Release Solution Configuration and build that solution.
The DLL generated will be placed in one of:
Windows-SDK-11-x64\build\Release_x64\xcomp\generic.dll
Windows-SDK-11-x64\build\Debug_x64\xcomp\generic.dll
macOS:
Building Studio resources (.RC files) for a component requires that the Studio resource compiler is added to the Xcode tree.
Copy tools/omnisrc64.app into:
/Applications/Xcode.app/Contents/Developer/Tools
Select the project file at:
/macOS_SDK_11/xcomp/generic/mac/generic.xcodeproj
and open with Xcode.
For a Deployment/Release build choose Product->Build For->Profiling (Shift-Cmnd-I) and for a Development/Debug build choose Product->Build For->Testing (Shift-Cmnd-U).
The resulting xcomp bundle will be placed at:
/OmnisSDKBuild/Release/xcomp/generic.u\_xcomp and /OmnisSDKBuild/Debug/xcomp/generic.u\_xcomp ( relative to the root of the SDK location ).
On macOS, a Release build will generate a Universal binary which targets both Intel and M series Silicon architectures and a Debug build will target only the platform it was built on.
Therefore, when creating a new component ensure that if linking with third party libraries those libraries are provided as Universal versions.
Linux:
Uses the CMake tool to automate the building of components. More information can be found at https://cmake.org.
All components (including generic.so) are built from the terminal line using the build_all.sh bash shell script at:
/linux-headless-sdk-11/master/linux/build\_all/build\_all.sh
Usage:
build_all.sh [-o|--outdir <dir>] [-d|--debug]
-o | --outdir <dir>: The root output directory for the build.
-d | --debug: Debug
--clean: Deletes the output directory before building.
To build a set of Release components change the current path to the folder containing the script and use:
./build_all.sh
This will place the resulting libraries at:
/linux-headless-sdk-11/output/Release/headless/Server/xcomp/
The generic component will build into:
/linux-headless-sdk-11/output/Release/headless/Server/xcomp/generic.so
Adding the -d switch will produce a set of debug libraries at:
/linux-headless-sdk-11/output/Debug/headless/Server/xcomp/
The default build and output path are created at the root of the SDK tree. To change this, specify a different path using the -o switch.
To use the component, place a copy in the XCOMP folder of the Studio tree.
To use the component, place a copy in the XCOMP folder within the Studio tree on Windows and the PlugIns folder within the Studio tree on macOS.
To add the component to the Component Store right click the Project Libraries node in the Studio Browser and select Show Comps.lbs.
The Component Library node will be shown. Select External Components from the class pane.
This will show the External components window from which a component in the tree can be selected.
Expand the External Components node and select the Generic Library.
From the Pre-Load Status options select Starting Omnis. This will load the component for use from the Component Store when Studio is started.
Now when the Component Store is used it will show the Generic Control in the External Components group and this can be dragged and dropped onto a window.
Any errors raised when loading the component will be reported in the Omnis trace log.
The generic example can be used as a template for creating a new component.
New components should use the same tree structure and project settings as generic with project source files added to the source folder.
This will ensure that the component SDK headers and libraries can be located and that a build is output to the expected location.
On Windows and macOS ensure the Visual Studio and Xcode project settings are based on the settings used for a sample component to ensure they build correctly.
On Linux to use a new component ( myxcomp ) with the CMake build tool modify the following files ( assuming this used generic as a template ).
/linux-headless-sdk-11/xcomp/myxcomp/linux/myxcomp/CMakeLists.txt
Alter the name of the project on line 2 from generic to myxcomp and update the set(RCFILE) and set(SOURCES) commands on the lines which follow to use the correct resource file and source files.
/linux-headless-sdk-11/xcomp/linux/all_xcomps/CMakeLists.txt
Add a line to the end of the file which will call the build macro for the new component.
add_xcomp_project("myxcomp")
Components which are not required can have their add_xcomp_project entry removed or commented out from this file.
Use the build_all.sh script to build the component via CMake.
By default, Omnis makes use of the Google Crashpad crash-reporting system to log the state of the program when a crash occurs.
To debug crashes in a component requires that Release versions of components contain debug symbols.
Therefore, the Release versions of the SDK sample components will use debug symbols and generate an associated symbol file.
The debug symbol file for the component binary can then be used with a Crashpad mini dump (.dmp) to provide information about a crash, e.g. stack trace and variable state.
Windows:
A Release build uses a PDB file as its symbol file and places this in:
\Windows-SDK-11-x64\intbuild\debugsymbols_x64\xcomp\<component_name>.pdb
Studio Crashpad mini dump files (.dmp) are placed in the Studio log at:
AppData\Local\Omnis Software\OS 11\logs\crashes\reports\reports\
The following provides an overview of how to use Visual Studio to open the dmp file, load the DLL and its PDB file to provide information about a crash.
https://learn.microsoft.com/en-us/visualstudio/debugger/using-dump-files?view=vs-2019
macOS:
A Deployment/Release build uses a dSYM file as its symbol file and places this in:
/OmnisSDKBuild/DebugSymbols/xcomp/<component\_name>.u\_xcomp.dSYM\ (relative to the root of the SDK location.)
Studio Crashpad mini dump files (.dmp) are placed in the Studio log at:
/Application\ Support/Omnis/Omnis\ Studio\ 11/logs/crashes/reports/pending/
Linux:
A Release build uses a .debug file as its symbol file and places this in,
/output/debugsymbols/headless/Release/xcomp/<component\_name>.so.debug ( relative to the output location specified when building ).
Studio Crashpad mini dump files (.dmp) are placed in the Studio log at,
/logs/crashes/reports/pending/
On both macOS and Linux the lldb command line debugger tool should be used with the dmp file and symbol file to provide information about a crash.
Typically, lldb starts with the path to the main executable which generated the crash and the dmp file.
macOS:
lldb /Applications/Omnis\ Studio\ 11.app --core <path_to_dmp_file>
Linux:
lldb /usr/local/omnis_software/omnis_studio_headless_app_server_11/homnis --core <path_to_dmp_file>
The external library module can be added to the debugger by using the target modules command with the location of the library binary and its symbol file.
https://lldb.llvm.org/use/symbolication.html#
For example:
macOS:
target modules add /Applications/Omnis\ Studio\ 11.app/Contents/MacOS/xcomp/myxcomp.u_xcomp/Contents/MacOS/myxcomp --symfile /Users/user/Documents/OmnisSDKBuild/DebugSymbols/xcomp/myxcomp.u_xcomp.dSYM
target modules add /Applications/Omnis\ Studio\ 11.app/Contents/MacOS/PlugIns/myxcomp.u_xcomp/Contents/MacOS/myxcomp --symfile /Users/user/Documents/OmnisSDKBuild/DebugSymbols/PlugIns/myxcomp.u_xcomp.dSYM
Linux:
target modules add /usr/local/omnis_software/omnis_studio_headless_app_server_11/xcomp/myxcomp.so '/home/user/output/debugsymbols/headless/Release/xcomp/myxcomp.so.debug'
The generic sources provide a general template for component code.
Generic.cpp
The main body of the component implementation is described here in more detail.
The includes:
#include <extcomp.he>
#include <extfval.he>
#include <hwnd.he>
#include <gdi.he>
#include "generic.he"
extcomp.he, extfval.he, hwnd.he and gdi.he are external component library header files. extcomp.he declares various external component specific APIs; extfval.he declares the data storage class; hwnd.he declares the child window API calls; gdi.he declares the graphical API calls; and generic.he defines the component class.
The component entry point and message procedure is as follows.
extern "C" qlong OmnisWNDPROC GenericWndProc(HWND hwnd, LPARAM Msg, WPARAM wParam, LPARAM lParam, EXTCompInfo* eci)
OmnisWNDPROC is a #define that the Omnis component library has setup. This defines some calling conventions that vary from platform to platform. For now this is all you really need to know. Next is the name, GenericWndProc. This is the message procedure Omnis calls with your child window messages. This procedure has the following parameters:
| HWND | hwnd | This is a handle to the child window the message is for |
| LPARAM | Msg | This is the message |
| WPARAM | wParam | This is extra information for the message |
| LPARAM | lParam | This is extra information for the message |
| EXTcompInfo* | eci | This is a pointer to some information about your component |
When Omnis calls the message procedure, it sends along the HWND in the hwnd parameter. This is the child window the message was for. Complex components may have many child windows all using the same message procedure. Using the HWND helps the component do the right thing for the right child window.
Msg is the message. There are many messages that can be sent to your procedure, such as WM_PAINT or WM_LBUTTONDOWN.
wParam is some extra information for the message. Sometimes messages need to pass other information, such as the cursor position when the WM_LBUTTONDOWN was generated.
lParam, like wParam, is used for extra message information.
eci is a pointer to a structure holding information about your component. It is used with various API calls. See later.
Next is the first and most important line of the message procedure.
ECOsetupCallbacks(hwnd, eci);
Most of the API calls call Omnis for some information, or to do some processing. This line enables the call to Omnis to work. If this line is missing, your component will crash.
Next there is a switch statement testing the message parameter:
ECM_GETCOMPLIBINFO. This is the first message the message procedure handles. Omnis is calling your message procedure trying to find out how many controls your component supports, and the name of your library.
return ECOreturnCompInfo( gInstLib, eci, LIB_RES_NAME, OBJECT_COUNT );
Here you return the result of a function call ECOreturnCompInfo. This function is described later, but usually takes a string resource number which holds the name of your component library, and takes the number of controls your component contains.
ECM_GETCOMPID. Omnis now knows how many controls you are intending to support in your component library due to the result of the last message. It now wants to know what ID each control within the component library should have. The id can be any number you decide to associate with the control. In the future when Omnis wants something to happen to a control, it uses the id you return here. Omnis calls you with this message for 1 to n times, where n is the number of controls your library supports. The calling count is passed in wParam.
if ( wParam==1 )
return ECOreturnCompID( gInstLib, eci, OBJECT_ID1, cObjType_Basic );
return 0L;
Since generic supports one control (OBJECT_COUNT=1, this is defined in your header file), you wait for a call where wParam is 1. On this message, you return the result of ECOreturnCompID. This API specifies the controls id, and the type of control you want it to be. See Types Of Component later in this document. Here you indicate the control has an id of OBJECT_ID1 and is a cObjType_Basic basic component.
ECM_GETCOMPICON. Now Omnis knows how many controls your library has and the id for each control, it asks for the icon to use in the Omnis Component Store.
if ( eci->mCompId==OBJECT_ID1 )
return ECOreturnIcon( gInstLib, eci, GENERIC_ICON );
return qfalse;
Here, you are checking a member of the eci parameter, mCompId. This is set to an id you returned from the last message (OBJECT_ID1). The ECOreturnIcon API is described later, but generally it extracts a .bmp ( bitmap ) from the resource file so you can return it to Omnis.
With regards to setting up your component so that Omnis knows it is there, these messages are generally all you need. The next set of messages are used when you place your component on a window or report class. When that happens, Omnis calls your message procedure with many more messages. Here are the important ones.
ECM_OBJCONSTRUCT. Omnis is calling the message procedure as it is just about to create an instance of your object. This can happen when you drag a component out of the Component Store on to a design window or report class, or a window class is being opened in runtime mode, or a report is being printed. This is the code you need to execute:
tqfGenericObject* object = new tqfGenericObject( hwnd );
ECOinsertObject( eci, hwnd, (void*)object );
return qtrue;
The first line creates a new object called tqfGenericObject, which is defined below. This class performs all of the operations for your control. Next it is calling ECOinsertObject. This API adds a pointer to the tqfGenericObject just created into a chain of objects. The pointer and the hwnd being passed are stored in the chain. The external component library maintains this chain, so later when a message arrives in your message procedure, you can ask for the object ( tqfGenericObject ) based on the child window the message was for, and get the correct object to process the message. This is necessary as multiple instances of your component can be created.
Finally you return qtrue. This informs Omnis you have processed the message. Not all messages expect qtrue to indicate the message was handled. The return value from all messages can be found in this manual.
ECM_OBJDESTRUCT is the next message. Omnis is calling the message procedure as it is just about to delete an instance of your object. This can happen when you close a window class containing your component, or a report has finished printing your component:
tqfGenericObject* object = tqfGenericObject*)ECOremoveObject(eci,hwnd);
if ( NULL!=object )
{
delete object;
}
return qtrue;
The first line here is calling ECOremoveObject to find an object in the chain of objects based on the passed hwnd. If an object is found, it is removed from the list and a pointer to the object is returned. If the pointer is valid, you delete it, freeing all memory previously allocated. Again, you return qtrue to inform Omnis you have processed the message.
Finally:
return WNDdefWindowProc(hwnd,Msg,wParam,lParam,eci);
This is another very important line. Remember that many messages are sent to the message procedure, some important, others not so important. This is where the not so important messages should go. WNDdefWindowProc is an API to which all messages not handled should be passed. This allows Omnis to do the default operation for messages you do not want to handle.
On previous messages, ECM_OBJCONSTRUCT and ECM_OBJDESTRUCT referred to the tqfGenericObject class. This class contains the code which makes the component actually do something. You can add to this later.
Generic.he
This defines the tqfGenericObject class declared in generic.cpp.
Generic.rc
This is the resource file. It is laid out like a Windows resource file. Under macOS, the Studio resource compiler (which is part of the SDK) must be used to support the basic set of Windows resource keywords. If you keep the resource files simple, they will be cross-platform.
The resource file first includes a bitmap (generic.bmp). You can either create a small Window .BMP file (16x16 preferably), or use a copy of the generic.bmp file.
String 31000 is very important, as this is the name of the message procedure that Omnis tries to call. If you do not have this string, the name of your message procedure should be ‘OmnisEXTCOMPONENT’. If the message procedure is not called this, and you do not have a string defining the name to call, Omnis will not call you.
Windows .DEF / Generic.def
Under Windows, an export file is needed for the compiler. This file describes what functions can be called from outside the component. As described above, Omnis needs to call your message procedure with messages for your child windows. For it to do that, the message procedures must be exported.
To extend the functionality of the first example the generic2 sample component implements adding a property to the component which will appear in the Property Manager. You can modify component properties in design mode and runtime using the Property Manager.
The definition of the tqfGenericObject in generic2.he adds a new member called mMyColor to the class. Its type is qcol. This is a type defined in gdi.he and represents a color value (RGB).
A new member function attributeSupport is also added. This function is used when Omnis is doing something with your properties.
The paint method in generic2.cpp uses some API calls from both hwnd.he and gdi.he. When this method is called as a result of a message, it fills your component with the color that is stored in the new color member mMyColor. You should read the HWND Reference and GDI Reference chapters for an explanation of the APIs used, but generally, the code gets the size of your child window (left, top, width, height), and gets a solid brush. It sets the color of the solid brush to the color in the new color member and then fills the child window with that color.
Consider the following messages in the GenericWndProc entry procedure in generic2.cpp:
WM_PAINT message informs use the hwnd needs painting.
ECM_GETPROPNAME is sent by Omnis to ask for the component’s property table.
ECM_PROPERTYCANASSIGN is sent by Omnis to see if a property can have values assigned.
ECM_SETPROPERTY is sent by Omnis to get the value of a property.
ECM_GETPROPERTY is sent by Omnis to set the value of a property.
When you get a WM_PAINT message, you find the object in the chain of object instances from the hwnd coming into the message procedure. If you find the object, you call the ::paint() member function of the object.
When you get a ECM_GETPROPNAME message, you call another ECO API to build a property table and return it to Omnis. This API is described later, see Component Properties.
const cMyColorProp = 1;
ECOproperty MyProperties[] =
{
cMyColorProp, 4000, fftInteger, EXTD_FLAG_PWINDCOL, 0, 0, 0
};
This table defines your properties. The layout of the table is described in the ECOproperty section of the Structures, Messages and Functions chapter, but generally it describes the property id, the resource name of the property, its data type, and the type of data as shown in the Property Manager. The property table, when returned to Omnis using the code shown below, controls how your properties are handled.
return ECOreturnProperties( gInstLib, eci, &MyProperties[0], 1 );
The attributeSupport method is called when Omnis needs to do something with your properties. The messages used in this method are covered in more detail in the ECM_PROPERTYCANASSIGN, ECM_GETPROPERTY and ECM_GETPROPERTY sections of the Structures, Messages and Functions chapter, but generally it lets you handle your color property or any future properties you decide to add. For this example, when a color property is assigned, you alter the member in the tqfGenericObject class with the new color value being sent from Omnis, force your child window to be repainted, resulting in the new color being drawn on screen. When the Property Manager needs to know what the color is, you send it the value back, and you also tell the Property Manager if it is allowed to assign color to your object.
The generic2.rc resource file also has an entry for the description of the $mycolor property.
After building the generic2 component, close Omnis if it is still running, and move the component into your XCOMP folder. Restart Omnis.
After building the generic2 component, close Omnis if it is still running, and move the component into your XCOMP or PlugIns folder. Restart Omnis.
In the Omnis IDE, when you open a window class and click on your component control, a Custom tab is displayed in the Property Manager. Select it and you should see your color property. Now try assigning some values and it should change the color of your component.
You have covered the very basics of building your own external component. The source contains further generic samples that build on from the basic one adding more properties, events, and component methods.
If you are ready for more of a challenge, the source has many other controls that demonstrate much more of the external components interface. All of the samples supplied are completely cross-platform.
Bear in mind Omnis is a cross-platform development tool, and the external component interface has been designed with this in mind. If you want your controls to run on all platforms supported in Omnis, try to use the Omnis API as much as possible. There is very little it cannot do, and if you only use the API, your code should remain completely portable, all you need to do is recompile.
Here are some general points you should remember when writing Omnis components:
Keep to the External Component API; this helps you port your controls to other platforms
Initialize all members used to handle properties. When the control is first created, the initial values, as displayed in the Property Manager, are the values you initialize your members to.
Read the ‘Memory Issues’ for the EXTfldval and EXTqlist classes later defined.
Do NOT nest painting. See WNDstartDraw and WNDendDraw in the HWND documentation.
For large amounts of data, such as picture components, you can use the MEMincAddr and MEMdecAddr function to handle large images.
If you have any problems with your component when you are within the Omnis IDE, such as the DLL not appearing in the #EXTCOMP dialog, check the Omnis trace log. Any problems encountered in Omnis with respect to your components are reported to the trace log.
String resource 31020 should not be used as it is reserved for Web Client version number checking.
If you declare a date property ( fftDate ), depending on the date subtype used with the EXTfldval::setDate() API, the Property Manager uses either #FT or #FDT to format the property value.
EXTfldval fval;
fval.setDate( myDateValue, dpFtime );
This example stores a date value in an EXTfldval object. The Property Manager would use #FT to format value because the date subtype used was dpFtime.
Please note that Xcode and its underlying build scripts may encounter problems where file or folder names contain spaces. For this reason it is best to use underscores in place of spaces. Alternatively, it may be possible to work around the issue by adding double quotes around various build attributes, for example:
The two arguments to the cp command that follows the omnisrc command in the rule for compiling rc files.
The header search path for the project headers.
The framework search path.
Ensure that your Linux system has the necessary GCC C/C++ libraries (version 7 or higher) installed in addition to the Clang compiler (version 8 or higher) and CMake build tool (version 3.15.5 or higher).
Check the installed version of GCC using:
gcc --version
This can be installed or updated using the package manager provided by the system, e.g. for Ubuntu:
sudo apt update
sudo apt install build-essential
To check the installed version of Clang use:
clang --version
If this needs to be installed or updated use the package manager provided by the system, e.g. for Ubuntu:
sudo apt install clang-8 --install-suggests
Check the version of CMake installed using:
cmake --version
Use the package manager provided by the system to update or install, e.g. for Ubuntu:
sudo apt install cmake
Non-visual components are component libraries which contain either Omnis static functions and/or Omnis external class objects.
Just like in high-level languages such as C++ static functions are useful when processing single non-related tasks. However when functions are related, it is sometimes useful to build a collection of related functions into a class object.
Static functions are functions which can be used in the Omnis script language in calculations. Component library static functions appear in the ‘Functions’ category in the Catalog window.
Adding static functions to your component library requires the following steps:
Add the flag EXT_FLAG_NVOBJECTS to the set of flags returned by ECM_CONNECT message. Without adding this flag, Omnis will not request the list of static functions from your library.
Return a list via ECOreturnMethods in response to a ECM_GETSTATICOBJECT message. This message is sent to the component library when Omnis requires a list of static functions.
Respond to ECM_METHODCALL. However, as these are static functions, the HWND parameter will be NULL. As will wParam and lParam.
An example of static functions in use would be (excerpts from FILEOPS):
ECOmethodEvent fileStaticFuncs[ cSMethod_Count ] =
{ // Unique external ID Resource Number Flags
cSMethodId_CreateDir , 8000, 0, 0, 0, 0, 0,
….
cSMethodId_Rename , 8014, 0, 0, 0, 0, 0
};
extern "C" qlong OmnisWNDPROC GenericWndProc(HWND hwnd, LPARAM Msg, WPARAM wParam, LPARAM lParam, EXTCompInfo* eci)
{
ECOsetupCallbacks(hwnd, eci);
switch (Msg)
{
case ECM_CONNECT:
{ // Component library contains NV objects & should always be loaded
return EXT_FLAG_LOADED|EXT_FLAG_ALWAYS_USABLE|EXT_FLAG_NVOBJECTS;
}
case ECM_GETSTATICOBJECT:
{ // Omnis is requesting a list of our static functions
return ECOreturnMethods( gInstLib, eci, &fileStaticFuncs[0], cSMethod_Count );
}
case ECM_METHODCALL:
{ // Omnis requires a static method to be called
switch ( ECOgetId(pEci) )
{
case cSMethodId_CreateDir:... Processing
…
case cSMethodId_Rename: … Processing
}
return 1L;
}
}
return DefWindowProc( hwnd,Msg,wParam,lParam,eci);
}
See also ECM_CONNECT, ECM_GETSTATICOBJECT, ECM_METHODCALL, ECOreturnMethods
Class objects are, as in the Omnis language, objects which group together data and functions into a single entity.
Adding class objects requires the following steps :-
Add the flag EXT_FLAG_NVOBJECTS to the set of flags returned by ECM_CONNECT message. Without adding this flag, Omnis will not request of list of objects from your library.
Respond to the ECM_GETOBJECT message and return a list of objects via ECOreturnObjects. It is important to note that your ECOobject structure should contain unique ids. During subsequent calls the EXTCompInfo mCompId member will contain this id to inform you of the type of object.
Respond to ECM_OBJCONSTRUCT, ECM_OBJDESTRUCT and ECM_OBJECT_COPY to ensure that your objects are created, destructed and copied.
Respond to ECM_GETMETHODNAME and ECM_GETPROPNAME to return any methods and properties that your object may have.
Respond to ECM_PROPERTYCANASSIGN, ECM_SETPROPERTY, ECM_GETPROPERTY in normal manual to manage your objects’ properties.
Finally, respond to ECM_METHODCALL to inform any of your objects’ methods.
It is important to note that during all of the above messages (except ECM_GETMETHODNAME, ECM_GETPROPNAME and ECM_OBJECT_COPY) lParam will contain a unique reference to your object. You should use ECOfindNVObject to retrieve your objects’ data.
It is also important to note how you require your objects to be managed. For example the FILEOPS example uses a container to hold the actual object. The object is only released/freed when a reference count gets to zero. This allows several Omnis object variables to point to the same FILEOPS object (similar to COM).
An example of use may be (excerpts from FILEOPS):
ECOobject fileObjects[ cObject_Count ] =
{ // Unique external ID Resource Number Flags
cObject_FileOps, 2000, 0, 0
};
extern "C" qlong OmnisWNDPROC GenericWndProc(HWND hwnd, LPARAM Msg, WPARAM wParam, LPARAM lParam, EXTCompInfo* eci)
{
ECOsetupCallbacks(hwnd, eci);
switch (Msg)
{
case ECM_CONNECT:
{ // Component library contains NV objects & should always be loaded
return EXT_FLAG_LOADED | EXT_FLAG_ALWAYS_USABLE | EXT_FLAG_NVOBJECTS;
}
case ECM_GETOBJECT:
{ // Omnis is requesting a list of our object class names
return ECOreturnObjects(gInstLib,eci, &fileObjects[0], cObject_Count);
}
case ECM_OBJCONSTRUCT:
{ // Omnis is requesting the construct of a new object.
if ( eci->mCompId==cObject_FileOps )
{
tqfFileOpsContainer* object = (tqfFileOpsContainer*) ECOfindNVObject(eci->mOmnisInstance, lParam );
if ( !object )
{
tqfFileOpsContainer* obj = new tqfFileOpsContainer((qobjinst)lParam);
ECOinsertNVObject(eci->mOmnisInstance,lParam,(void*)obj);
}
return qtrue;
}
return qfalse;
}
case ECM_OBJDESTRUCT:
{ // Omnis is requesting the destruction of your object
if (eci->mCompId==cObject_FileOps && wParam==ECM_WPARAM_OBJINFO)
{
void* object=ECOremoveNVObject(eci->mOmnisInstance,lParam );
if ( object )
{
tqfFileOpsContainer* fileOps = (tqfFileOpsContainer*)object;
delete fileOps;
}
}
return qtrue;
}
case ECM_OBJECT_COPY:
{ // Omnis requires a new object to be created from an existing one
objCopyInfo* copyInfo = (objCopyInfo*)lParam;
tqfFileOpsContainer* srcobj = (tqfFileOpsContainer*) ECOfindNVObject(eci->mOmnisInstance,copyInfo->mSourceObject);
if ( srcobj )
{
tqfFileOpsContainer* destObj = qfFileOpsContainer*)ECOfindNVObject(eci->mOmnisInstance, copyInfo->mDestinationObject );
if ( !destObj )
{
destObj = new tqfFileOpsContainer( (qobjinst)copyInfo->mDestinationObject,srcobj);
ECOinsertNVObject( eci->mOmnisInstance, copyInfo->mDestinationObject, (void*)destObj );
}
else
destObj->setObject( (qobjinst)copyInfo->mDestinationObject,srcobj );
}
break;
}
case ECM_GETMETHODNAME:
{ // Omnis is requesting a list of our objects’ methods
if ( eci->mCompId==cObject_FileOps )
return ECOreturnMethods( gInstLib, eci, &fileObjFuncs[0], cIMethod_Count );
break;
}
case ECM_GETPROPNAME:
{ // Omnis is requesting a list of our objects’ properties
// but we don’t have any so simply return
break;
}
case ECM_PROPERTYCANASSIGN:
case ECM_SETPROPERTY:
case ECM_GETPROPERTY:
{ // Omnis requires property management
// but we don’t have any so simply return
break;
}
case ECM_METHODCALL:
{ // Omnis requires a method to be invoked
if ( eci->mCompId==cObject_FileOps )
{
void* object = (void*)ECOfindNVObject( eci->mOmnisInstance, lParam );
tqfFileOpsContainer* fileOps = (tqfFileOpsContainer*)object;
if ( fileOps->mObject )
return fileOps->mObject->methodCall( Msg, wParam, lParam, eci );
}
break;
}
}
return DefWindowProc( hwnd,Msg,wParam,lParam,eci);
}
OmnisObject is a base class that can be used for representing an external object that is being implemented. OmnisObjectContainer is a container for OmnisObject objects which could then be cast back to the external object class when needed.
These base classes provide a starting point and a container for multiple external component classes – you could offer more object classes from one external component project (e.g. workers and non-visual objects) and keep your classes in a OmnisObjectContainer.
Furthermore, these track the number of references internally and delete themselves when references reach 0, in line with other Omnis external components.
This section describes how to develop a control handler component. Control handlers are essentially components which handle other components, such as an ActiveX.
To create a control handler, you should follow these steps. Note that the CONTROLLIB and CONTROL classes are used for illustration purposes and do not exist in the Omnis component environment.
Add EXT_FLAG_CTRLHANDLER to the component flags.
Process ECM_GETHANDLERICON to inform Omnis of the HBITMAP to use for the components' group in the Component Store.
Support for ECM_GETCOMPLIBINFO must be restructured. The component must provide the information for all the control libraries that it supports. The control library names that the component supports must include a file path as a prefix. An example of this would be:
if ( !pEci->mCompLibId )
{
// Omnis is inquiring on the handler.
ECOreturnCompInfo(gInstLib,pEci,CTRL_RES_NAME,0);
// Id of first library
pEci->mCompLibId = 1;
return qtrue;
}
else
{
// Omnis is inquiring on a control libraries
CONTROLLIB* prevLib = getLib( pEci->mCompLibId );
// Find library from id
CONTROLLIB* nextLib=0;
if ( prevLib )
nextLib=prevLib->mNextLibrary;
if ( nextLib )
{
// you have another library for Omnis
EXTfldval exfldval;
// Format of returned name <FilePath>+'\0'+<Library Name>
str255 ctrlInfo = nextLib->mLibraryPath;
// Space for NULL
ctrlInfo[0]++;
// Terminate Cstring
ctrlInfo[ ctrlInfo.Length() ] = '\0';
// Add control library name
ctrlInfo.concat( nextLib->mLibraryName );
exfldval.setChar( ctrlInfo );
ECOaddParam(pEci,&exfldval);
// Set Unique id of this library. The id may change between sessions.
pEci->mCompLibId = nextLib->mLibraryId;
// Return number of controls within library
return nextLib->mControlCount;
}
// No more libraries
return qfalse;
}
CONTROLLIB* curLib = getLib( pEci->mCompLibId );
if ( curLib )
{
EXTfldval exfldval;
exfldval.setChar( curLib->getControlName(wParam) );
ECOaddParam(eci,&exfldval);
pEci->mCompId = curLib->getControlId( wParam );
}
return cObjType_Basic;
// wParam is true if the library is to be loaded. This enables fastest
// load time as it avoids loading the bitmap for every library that
// the handler supports.
if ( wParam )
{
CONTROL* control = getControl( pEci->mCompLibId, pEci->mCompId );
if (control)
{
EXTfldval exfldval;
exfldval.setLong( (qlong) control ->getHBitMap() );
ECOaddParam(eci,&exfldval);
// Bitmap returned
return qtrue;
}
}
// No bitmap returned
return qfalse;
CONTROLLIB* curLib = getLib( pEci->mCompLibId );
if ( curLib )
{
EXTfldval extfldval;
EXTqlist list; list.clear( listVlen );
for ( qshort constCount=1; constCount<=curLib->mConstantCount; constCount++ )
{
EXTfldval cval; qlong line = list.insline();
// Constant ID
list.getColValRef( line , 1, cval, qtrue );
cval.setLong( curLib->getConstId(constCount) );
// Constant String
list.getColValRef( line , 2, cval, qtrue );
cval.setChar( curLib->getConstantName(constCount) );
}
extfldval.setList( list, qtrue);
ECOaddParam(pEci,&extfldval);
return qtrue;
}
// No constants
return qfalse;
CONTROL* cntrl = getControl( pEci->mCompLibId, pEci->mCompId);
if (cntrl)
{
EXTfldval extfldval;
EXTqlist list; list.clear( listVlen );
for ( qshort num=1; num <= cntrl->getCount(); num ++ )
{
EXTfldval cval;
qlong line = list.insertLine();
// External id
list.getColValRef( line , 1, cval, qtrue );
cval.setLong(cntrl->getId(num));
// Name
list.getColValRef( line, 2, cval, qtrue );
cval.setChar(cntrl ->getName(num) );
// fft type of property/return type
list.getColValRef( line , 3, cval, qtrue );
cval.setLong(cntrl->getType(num) );
// EXTD_ flags
list.getColValRef( line, 4, cval, qtrue );
cval.setLong(cntrl ->getFlags(num) );
if ( ECM_GETPROPNAME==message )
{
// For properties you need to set the constant range
// Const Start (zero if none)
list.getColValRef( line , 6, cval, qtrue );
cval.setLong(cntrl->getConstStart(num));
// Const End (zero if none)
list.getColValRef( line , 7, cval, qtrue );
cval.setLong(cntrl->getConstEnd(num));
}
else
{
// For functions & events you need to add a
// list containing parameters
EXTqlist paramlist;
paramlist.clear( listVlen );
for ( qshort m=1; m<=cntrl->getParamCount(); m++ )
{
qlong paramline = paramlist.insertLine();
// Parameter name
paramlist.getColValRef( paramline, 1, cval, qtrue);
cval.setChar(cntrl->getParamName(m));
// fft Data type
paramlist.getColValRef( paramline, 2, cval, qtrue);
cval.setLong(cntrl->getParamType(m));
// EXTD_ flags
paramlist.getColValRef( paramline, 3, cval, qtrue);
cval.setLong( cntrl->getParamFlags(m) );
}
list.getColValRef( line , 6, cval, qtrue );
cval.setlist( paramlist, qtrue );
}
}
extfldval.setList( list, qtrue);
ECOaddParam(pEci,&extfldval);
return qtrue;
}
// No properties
return qfalse;
See also ECM_CONNECT, ECM_GETHANDLERICON, ECM_GETCOMPLIBINFO, ECM_GETCOMPID, ECM_GETCOMPICON, ECM_GETCONSTNAME, ECM_GETPROPNAME, ECM_GETEVENTNAME, ECM_GETMETHODNAME.
When creating a background external component, you need to be aware of the differences between real components, and of the extra messages you may need to respond to.
A background component is created in a different way within Omnis during runtime and design mode. When you are designing a background component in design mode, the component will be given a child window ( HWND ) to draw within. During design mode, Omnis maintains this child window. During runtime, no child window is created. Omnis will call your object to paint, in an existing window at a certain location. Given this runtime/design mode difference, you should not use any HWND API that requires a window, such as WNDsetCapture() as you may not have a valid child window.
The following messages describe the differences or meaning when received by a background component.
ECM_OBJCONSTRUCT is sent to all component types. For background components you can test the wParam parameter and the ECM_WFLAG_NOHWND flag to tell if you are being created during design or runtime. For example :
case ECM_OBJCONSTRUCT:
{
tqfTile* object = new tqfTile( hwnd );
object->mIsRealHWND = !(wParam & ECM_WFLAG_NOHWND);
ECOinsertObject( eci, hwnd, (void*)object, wParam );
return qtrue;
}
The above example creates a new background component object, and stores a flag in the class so the control knows if it has a real child window or not.
The ECM_CONNECT needs to be handled for background external components. When Omnis calls your component with this message, the following code should be used. If the code is omitted, Omnis will create the control as a first class foreground object.
case ECM_CONNECT:
{
return EXT_FLAG_LOADED | EXT_FLAG_BCOMPONENTS;
}
ECM_PRINT is a very important message. Normally with standard components you pick up the WM_PAINT message so you can paint your control. During runtime, as you do not have a child window, you will never receive a WM_PAINT message. During design mode you do have a child window, so in theory you could get a WM_PAINT message, but you will not. To help background components keep a simple interface, Omnis sends only ECM_PRINT to your component during runtime and design mode to indicate that it needs to be painted. A WNDpaintStruct is passed in the lParam parameter which holds the area that needs painting and a HDC to paint within.
case ECM_PRINT:
{
tqfTile* object = (tqfTile*)ECOfindObject( eci->mOmnisInstance, hwnd, wParam );
WNDpaintStruct* ps = (WNDpaintStruct*)lParam;
if ( object ) object->paint( ps->hdc, &ps->rcPaint );
return qtrue;
}
The above example shows how to paint you background object in runtime or design mode.
Writing Web Client components is almost identical to writing standard window components. In fact, you can build both from the same source. There are however some small differences.
You will need to link against a different set of libraries. Use the example project files as a guide.
The final DLL/shared library name must match the component library name as specified by your resources. The library name resource ID is returned by ECOreturnCompInfo as a response to the ECM_GETCOMPLIBINFO. As a rule of thumb, all our web client component names start with “FORM”, i.e. FORMTREE, FORMTIME, etc.
Web client components must respond to the ECM_GETCOMPSTOREGROUP message and return the group name with ECOreturnCStoreGrpName ECM_GETVERSION (see Chapter 2—Components Reference). The group name must be “WEB Components” for web controls and “WEB Background Objects” for web background objects.
Web client components must be data bound in order to manipulate Omnis data. There is no other way of telling the client that data has changed and needs to be sent to the server for the next event. There is a message ECM_HASPRIMARYDATACHANGED (see Chapter 2—Components Reference) which the component needs to implement. You use it to tell the web client if the primary data has been changed by the user. If the component only displays data, it can have non-data bound properties which take instance variable names, but the component will not know when the data has changed.
Web client components need to implement the following additional messages which deal with focusing and mouse clicks.
ECM_CANFOCUS
ECM_ CANCLICK
(see Chapter 2—Components Reference)
In addition, web client components must implement a proper versioning system. There is a new ECOreturnVersion function which must be used as a response to ECM_GETVERSION (see Chapter 2—Components Reference).
ECOsendEvent function will always return true when called from web client components. In order to receive a result, the component must implement the ECM_EVENTRESULT message (see Chapter 2—Components Reference).
Some functions or classes require additional parameters when used from web client components. These are
EXTBMPref::EXTBMPref
EXTCURref::EXTCURref
(see Chapter 3—EXTBMPref/EXTCURref Class Reference)
The EXTfile class and related functions are currently not supported.
The new generic stationary in the MACIDE folder (Mac only) includes basic code needed for writing web client components.