Feature #4438
parsing and conversion of .wrx files
90%
Related issues
History
#1 Updated by Greg Shah over 4 years ago
The following is copied from #3767.
From Marius:
A .wrx file is MS CFB file (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b) a general-purpose file format that provides a file-system-like structure within a file for the storage of arbitrary, application-specific streams of data. This low level format is used by other Microsoft documents like
.doc
(Word document),.msg
(mail message), etc. For example you can try to rename a.wrx
file as.doc
file and you will see that Word and LibreOffice Writer are able to open the file.The problem is that the content is in binary format specific for each application
.wrx
,.msg
,.doc
..msg
file structure is documented by MS but I'm afraid the .wrx binary format content is created by Progress Software and not documented. https://knowledgebase.progress.com/articles/Article/What-do-wrx-files-containSome time ago I created a Python script able to parse
.msg
files and extract mail properties and attachments.
From Greg:
A .wrx file is MS CFB file
This is an interesting detail. I had assumed it was something completely proprietary to the 4GL. Looking through the specification, I note there is this "OLE Property Sets" specification:
I wonder if Progress just implemented such an approach.
All settings I did at runtime are stored perhaps inside .wrx file and are loaded at runtime during initialization!
Yes, this is something we need to resolve. I agree that parsing the
.wrx
file is the best approach. I do prefer to convert any such design-time features into static code references. In other words, I want to eliminate the.wrx
file itself after conversion. Since one cannot edit that file easily (or at all) and it is difficult (impossible?) to read the settings later, it is a terrible way to store settings. It is much better to leave this as code that can be edited and read.The down side is the scenario where there are more than one
.wrx
conditionally used in the sameLoadControls
. I've never seen such an approach, though it should be possible. In such a case we would also have to write the settings code in a conditional manner. For now we can ignore this problem.Please look into the
.wrx
format and see if you can decode it. Perhaps there is some generic tool for viewing/parsing these files that can at least be used to understand the format. I'm going to create a separate task for this so that the findings will be public.
#2 Updated by Greg Shah over 4 years ago
Any parsing we do would need to be using Java. Each of the properties set from the .wrx
would be converted into OCX setter calls in the converted code. The .wrx
would not be used at runtime.
#3 Updated by Vladimir Tsichevski over 4 years ago
Greg Shah wrote:
Any parsing we do would need to be using Java. Each of the properties set from the
.wrx
would be converted into OCX setter calls in the converted code. The.wrx
would not be used at runtime.
I see a possible trouble here. I suspect, that the .wrx
files contain setters for all the OCX properties, which were exposed by the original OCX at the moment the .wrx
file was created, most of these being just default values, which need not be set.
At the moment, is is safe to not implement some of original OCX properties, if applications does not set them. Implementing the proposed .wrx
file processing will broke these OCX implementations.
To solve this, the conversion program may statically check the implemented in FWD OCX interface and do not convert the missing properties, probably, just reporting them.
Moreover, I suppose most of the real .wrx
files contains only the default values, so no code at all should be generated for these files.
To make it happen, we should let the conversion program to know all the default property values. These we can, for example, define in the ComProperty
annotaions for property setters.
#5 Updated by Greg Shah over 4 years ago
To make it happen, we should let the conversion program to know all the default property values. These we can, for example, define in the ComProperty annotaions for property setters.
Good idea.
#6 Updated by Marius Gligor over 4 years ago
- File wmp.png added
- Status changed from New to WIP
- File cfb-explorer.png added
- File pbt2-wrx.png added
- File pbt3-wrx.png added
- File pbt-wrx.png added
1. I created an OE application using Application Builder using a SmartWondow on which I dropped a Progress Timer Control OCX. I saved the application and the .wrx file content was captured in picture pbt-wrx.png
Then I changed the OCX property Interval
from 0 to 3 and I saved the changes. Looking in the .wrx file pbt2-wrx.png
I saw that my change was saved, which means that any property changes at design time are saved in the wrx file. Next I added another OCX control, Windows Media Player to the same frame, I did no property settings and I saved my application. Looking in the wrx file pbt3-wrx.png
I found data for WMP. WMP has a property uiMode
of type string having a default value full
As we can see the unicode string value is in the .wrx file without doing any changes at design time. Looks like that properties values at design time are saved in the .wrx file. Basically the state of OCX properties at design time are saved, touched and untouched properties as well.
From Progress documentation related to ActiveX I extracted:
Unlike 4GL widgets, which have a standard set of defaults for the common attributes, the default settings for common ActiveX control properties vary from control to control. If you observe unexpected run-time behavior from an ActiveX control, consider whether you have set design time property values that are appropriate for your application. At runtime, your application uses AppBuilder-generated code to instantiate any ActiveX controls. This code (shown in following sections) uses the CREATE Widget statement to realize a separate control-frame widget for each ActiveX control, initializing each control-frame with the name specified at design time. It then invokes the LoadControls( ) method on each control-frame COM object to instantiate the corresponding ActiveX control, loading all design time definitions from the .wrx file.
which come to validate what I found.
2. Searching on internet I found a tool OpenMCDF
(https://sourceforge.net/projects/openmcdf) which allow to see what's inside a wrx file. The tool is a .NET application written in C# and runs only on Windows. Using this tool I opened the wrx file created previous. As you can see in the picture the content looks like a file system structure which contains a root directory and sub folders for each defined ControlFrame
. Because we have 2 OCX's we have 2 ContolFrame's.
From Progress documentation:
A single control-frame is capable of holding only a single ActiveX control. However, you can include many control-frames (and thus many ActiveX controls) in a Progress frame. This limitation incurs certain restrictions on the types of ActiveX controls that you can use. For instance, there is no internal support for container controls — that is, controls that allow multiple controls to reside inside of them.
We have also 2 files for each OCX, Frame
and Control0
, which store properties values. If you look at the OCX properties in an Application Builder, there are some properties which not belong to the OCX, but seems to be properties for Frame. Example: HonorProKeys
and HonorRetunKey
properties. Perhaps these properties are stored in the Frame
file and all OCX properties in file Control0
- Having settings in .wrx file for more than one OCX should not be a problem because we have the OCX name as an identifier. We already use the control name in
.ext-hints
files - Looking over properties data in .wrx file I didn't found any property name as a string! The problem is how we can associate a value to a property?
- At a first view the binary format of data seems to be a custom format but I'm not really 100% sure.
#7 Updated by Greg Shah over 4 years ago
Looking over properties data in .wrx file I didn't found any property name as a string! The problem is how we can associate a value to a property?
Take a look at the OLE Property Sets specification. COM is based on OLE. Each property is represented by a property ID and the string name is optional. I guess that OLE has some way to map these IDs to the specific properties being referenced. Each control is just a DLL after all, with various function entry points (COM methods) and exported memory locations (COM properties) which are defined as the external API of the DLL. It is common in Windows DLLs to assign ordinal values to exported entries. Perhaps the property ID is somehow related to that ordinal.
#8 Updated by Vladimir Tsichevski over 4 years ago
Greg Shah wrote:
Looking over properties data in .wrx file I didn't found any property name as a string! The problem is how we can associate a value to a property?
Take a look at the OLE Property Sets specification. COM is based on OLE. Each property is represented by a property ID and the string name is optional. I guess that OLE has some way to map these IDs to the specific properties being referenced. Each control is just a DLL after all, with various function entry points (COM methods) and exported memory locations (COM properties) which are defined as the external API of the DLL. It is common in Windows DLLs to assign ordinal values to exported entries. Perhaps the property ID is somehow related to that ordinal.
If I remember this correctly, the individual COM methods and properties are not exported as DLL entries. Only a few entries are required:
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID ) STDAPI DllCanUnloadNow(void) STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) STDAPI DllRegisterServer(void) STDAPI DllUnregisterServer(void)
And I suppose, the DllGetClassObject
returns the interface descriptions we are looking for.
#9 Updated by Hynek Cihlar over 4 years ago
Greg Shah wrote:
Looking over properties data in .wrx file I didn't found any property name as a string! The problem is how we can associate a value to a property?
Take a look at the OLE Property Sets specification. COM is based on OLE. Each property is represented by a property ID and the string name is optional. I guess that OLE has some way to map these IDs to the specific properties being referenced. Each control is just a DLL after all, with various function entry points (COM methods) and exported memory locations (COM properties) which are defined as the external API of the DLL. It is common in Windows DLLs to assign ordinal values to exported entries. Perhaps the property ID is somehow related to that ordinal.
COM methods (and properties) can be invoked dynamically through the optional IDispatch
interface. If you look at its Invoke
method, it declares a DISPID parameter, which is an ordinal value of the method/prop being invoked, see https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-idispatch-invoke. The dispid is defined in a type library AFAIK. Also there is another method IDispatch::GetIDsOfNames
which can translate the ids to names, I guess it goes to the type library and reads the info there.
I wonder if this dispid is somehow stored in the wrx
file.
#10 Updated by Marius Gligor over 4 years ago
- File ps-timer-tlb.txt added
- File ps-timer-props.png added
1. I started to do a decoding on .wrx file content.
Using Application Builder and PSTimer OCX which is a very simple ActiveX I created a simple procedure to check PSTimer
property values at design time. All PSTimer
properties which are exposed at design time are show in the picture.
Then, at design time, I changed OCX properties, one by one, I saved the .wrx file and I tried to find where properties are saved.
After doing that, at this stage, I'm able to describe the structure of .wrx file binary content as follow:
Offset Data ------------------------------------------------------------------------ 0000: 00 00 00 00 00 00 00 00 1C 00 00 00 1C 00 00 00 // ??? 16 bytes Unknown header 0010: 26 7b 46 30 42 38 38 41 39 30 2d 46 35 44 41 2d 31 31 43 46 2d 42 35 34 35 2d 30 30 32 30 41 46 36 45 44 33 35 41 7d (38) {F0B88A90-F5DA-11CF-B545-0020AF6ED35A} // OCX CLSID First byte 0x26 (38) is the length of the string 0037: 07 50 53 54 69 6d 65 72 // OCX name not to be confused with Name property. First byte 0x07 (7) is the length of the string PSTimer 003F: 48 00 00 00 - 34 00 45 00 39 00 33 00 44 00 39 00 37 00 31 00 2d 00 37 00 35 00 34 00 30 00 2d 00 31 00 31 00 62 00 31 00 (72) 2d 00 38 00 39 00 61 00 31 00 2d 00 30 00 30 00 43 00 30 00 34 00 46 00 44 00 34 00 41 00 38 00 33 00 36 00 4E93D971-7540-11B1-89A1-00C04FD4A836 // an unknown UTF-16LE UUID, First 4 bytes 0x00000048 (72) is the length of the string // BEG:OCX Here OCX properties are stored 008B: 15 00 00 00 // block size 0090 00 00 01 00 e5 02 00 00 e5 02 00 00 40 00 00 00 // ??? unknown 16 bytes. Could be something related to "About" property? I think About property should display a // copy rights message in a modal dialog box but does not work in Application Builder for all OCX I tested. // When "About" button is pressed Application Builder hangs! // OCX DATA STREAM - Here are OCX property values stored like a data stream. 009F: 01 // Enabled = True. (000B = VT_Bool - type is from type library, not stored in .wrx) 00A0: 07 00 00 00 // Interval = 7. (0003 = VT_I4 - type is from type library, not stored in .wrx) // END:OCX 00A4 = 008B + 15 + 4 00A4: 08 00 00 00 // ??? unknown data 00A8: 07 50 53 54 69 6d 65 72 // Name = PSTimer. First byte 0x07 (7) is the length of the string 00B0: 00 00 ff ff ff ff // ??? unknown data 00B6: 03 6D 61 67 // Tag = mag. 00BA: 01 00 00 00 // HonorProKeys = True. 00BE: 01 00 00 00 // HonorReturnKey = True. ...
I should analyze and compare another .wrx binary data for another OCX control, maybe we could find more regarding bynary data pattern.
2. Next, I found a small tool which allow us to dump a COM type library if exists: (http://theircorp.byethost11.com/index.php?vw=TypeLib)
According to MS specifications, Type Libraries are not mandatory to exists. If type library is missing we can query COM object to get information regarding exposed interfaces, properties an methods. For PSTimer
properties from Type Library dump I found:
---------------------------------------- Properties: ---------------------------------------- ID: FFFFFDFE ==-514 Name: 00000030 ==> "Enabled" Reference: 00000018 Record size (low-byte): 0014 Property number?: 0001 Flags: 800B = VT_Bool DataType: 000B = VT_Bool Variable kind: 000C = RequestEdit, Bindable Variable desc size: 0000 Value/Offset: 00240003 Unknown: 00000000 ---------------------------------------- ID: 00000001 == 1 Name: 00000044 ==> "Interval" Reference: 0000002C Record size (low-byte): 0018 Property number?: 0002 Flags: 8003 = VT_I4 DataType: 0003 = VT_I4 Variable kind: 0000 = Variable desc size: 0000 Value/Offset: 00240003 Unknown: 00000000 HelpContext: 000003EC
Basically inside TLB
we have properties metadata. For each property we have ID
, Name
, Propery number
, DataType
, etc.
3. My first conclusions are:
- Binary format is a Progress Software custom structure and is undocumented.
- Offsets in .wrx file are variable not fixed.
- OCX properties are stored together with wrapper properties.
- HonorProKeys
, HonorReturnKey
, Name
and Tag
are not OCX properties, they are wrapper properties. These properties are present in the property page for all OCX's.
- Enabled
and Interval
are OCX properties and the values are stored like a stream of data, one after the other. Only these properties should be decoded in FWD.
- Unfortunately I found no metadata (index, order, type, name) for OCX properties inside .wrx file!
- Looks like Progress decode OCX properties from .wrx file by querying OCX for metadata and then decoding values from data stream one by one using the Property number
and DataType
.
- Property ID's are not in order, Enabled ID = -514
and Interval ID = 1
, that's why I think Property number is used because Property numbers are in order 1,2,3,..
- Property ID
or Name
is probably used when property value is set.
- Some OCX properties could be read only. If values for those properties are in the data stream, they should be skipped.
- LE (little endian) format is used to store data.
- Another import issues is to check if the binary values can be decoded into Java types boolean, short, integer, long, float, double, string, etc My concern at this moment is about the double and float values. If they are standard IEEE 754 values should not be a problem.
4. In FWD we cannot depend on OCX instance nor in Type Library. IMO one possible solution is to keep OCX properties metadata in a XML structure like:
values <?xml version="1.0" encoding="UTF-8"?> <ocx clsid="{F0B88A90-F5DA-11CF-B545-0020AF6ED35A}" type="PSTimer"> <property number="1" type="VT_Bool" skip="true">Enabled</property> <property number="2" type="VT_I4">Interval</property> </ocx>
This file should be created once and used whenever we need to decode values for same OCX type (CLSID). The link between metadata from XML file and .wrx data is the OCX CLSID which is unique. Here we can keep among the property number and type attributes a skip attribute which specify if the property should be set or not. That is, we decode property value but we don't set the property value, we just skip over.
Obviously, any other ideas are always welcome.
#11 Updated by Greg Shah over 4 years ago
This is very good work!
Your point about the Appbuilder made me think about the fact that the original Appbuilder code is open source written in the 4GL. My idea was to check if the binary .wrx
writing code was included. I looked in the Possenet code for references to .wrx
files. In src/adeuib/_recreat.p
we can see this code:
/* Save the control to the wrx file using the control-frame name */ f_U._COM-HANDLE:SaveControls(tempBinaryFile, f_U._NAME).
I believe that SaveControls
is a 4GL language feature (probably written in C like the rest of the 4GL) which saves off the binary .wrx
file. It seems to be the design-time mirror image to the LoadControls
method that is also on the CONTROL-FRAME
.
Anyway, it seems like you already have enough information to parse the parts of the file that we care about. That is great!
In FWD we cannot depend on OCX instance nor in Type Library. IMO one possible solution is to keep OCX properties metadata in a XML structure like:
...
This file should be created once and used whenever we need to decode values for same OCX type (CLSID). The link between metadata from XML file and .wrx data is the OCX CLSID which is unique. Here we can keep among the property number and type attributes a skip attribute which specify if the property should be set or not. That is, we decode property value but we don't set the property value, we just skip over.
I like this idea. We would have to write a tool (using native code, similar to src/native/get_text_metrics.c
) which would capture this based on some list of OCX controls. Then we would store the result in a repo.
I don't quite understand the skip
idea. I think it is more important to know the default value for each property. If the value encoded in the wrx is different from the default, then we would write the property setter out into the Java code, otherwise we ignore it.
#12 Updated by Marius Gligor over 4 years ago
Greg Shah wrote:
[...]
Anyway, it seems like you already have enough information to parse the parts of the file that we care about. That is great!
[...]I like this idea. We would have to write a tool (using native code, similar to
src/native/get_text_metrics.c
) which would capture this based on some list of OCX controls. Then we would store the result in a repo.
[...]I don't quite understand the
skip
idea. I think it is more important to know the default value for each property. If the value encoded in the wrx is different from the default, then we would write the property setter out into the Java code, otherwise we ignore it.
1. We are close to have enough information to decode OCX property values from .wrx
files. The name of the OCX control is also important even though is not an OCX property. So far we have some conclusions as a result of analyzing data for PSTimer OCX only. This is not enough that's why I'm going to analyse data for other OCX as well, for example ProgressBar.
2. Reading data from (scanning) .wrx
files is very easy to implement. Using Apache POI (https://poi.apache.org/) we can do that in just a few lines of code. Apache POI is an open source project available under APACHE LICENSE, VERSION 2.0
3. Yes, we have to create a tool to extract metadata from OCX TLB and build the XML file, otherwise we should do that by hand. This tool will work only in Windows OS, we cannot do that in Linux. Nowadays almost all in Windows are .NET based. I have no idea on how we can do that in C code! Also I saw some posts which claims that using Powershell it is possible to analyze COM objects. A Powershell script would be better because is easy to maintain and is ready to run, no compile is needed. The last Windows C program I wrote is the p2j winspawn.c, 5-6 years ago!!!
4. Once we have the XML metadata, parsing data should be a trivial implementation. I proposed the skip
attribute for unimplemented properties. On the other hand for unimplemented properties, conversion rules will add a noop-com
annotation which in turn will generate a no operation COM chain, so we don't need a skip attribute for this purpose.
#13 Updated by Greg Shah over 4 years ago
Using Apache POI (https://poi.apache.org/) we can do that in just a few lines of code. Apache POI is an open source project available under APACHE LICENSE, VERSION 2.0
This is acceptable. You will have to update the gradle dependencies to add this.
This tool will work only in Windows OS, we cannot do that in Linux. Nowadays almost all in Windows are .NET based. I have no idea on how we can do that in C code! Also I saw some posts which claims that using Powershell it is possible to analyze COM objects. A Powershell script would be better because is easy to maintain and is ready to run, no compile is needed. The last Windows C program I wrote is the p2j winspawn.c, 5-6 years ago!!!
We already have some native code that does this. For example, you can see in src/native/comauto_win.c
, there are C functions like getComName(int id)
, getComProperty(int id, String name)
, getMethodType(int id)
. I'm hoping that with our existing code with uses the IDispatch API, that you can see how GetTypeInfo()
and other such helpers work.
I prefer to extend this code with any needed inspection functions and then write Java code to do the rest.
#14 Updated by Marius Gligor over 4 years ago
- File ms-common-60-tlb.txt added
- File progress-bar-props.png added
- File pb-custom-props.png added
- File ps-timer-tlb.txt added
1. Doing a decoding only on a simple OCX like PSTimer is not enough to conclude about .wrx file binary content format. So, I tried to do the same decoding for a more complex OCX. I decided to use a ProgressBar
control from Microsoft Windows Common Controls 6.0 (SP6)
. Using Application Builder I dropped a ProgressBar OCX control on a SmartWindow, then I opened the Property Editor to see which properties are available for change at design time.
The picture bellow shows the Property Editor page having default values.
When we choose the Custom option, a dialog window is open allowing us to change the values for a set of properties.
Choosing About
option or trying to change MouseIncon
property, will made Application Builder to hangs! In general Application Builder is unstable, he often hangs!
Then, I used a type library tool (http://www.benf.org/other/olewoo/index.html) to dump the Microsoft Windows Common Controls 6.0 (SP6)
OCX Type Library into a text file. (see attached ms-common-60-tlb.txt
)
From ms-common-60-tlb.txt
file, I selected IProgressBar
properties having attributes propget
, propput
and not hidden
as follow:
[id(0x00000001), propget, helpstring("Returns/sets a control's maximum value."), helpcontext(0x00033486)] HRESULT Max([out, retval] float* pfMax); [id(0x00000001), propput, helpstring("Returns/sets a control's maximum value."), helpcontext(0x00033486)] HRESULT Max([in] float pfMax); [id(0x00000002), propget, helpstring("Returns/sets a control's minimum value."), helpcontext(0x00033487)] HRESULT Min([out, retval] float* pfMin); [id(0x00000002), propput, helpstring("Returns/sets a control's minimum value."), helpcontext(0x00033487)] HRESULT Min([in] float pfMin); [id(0x00000003), propget, helpstring("Returns/sets the type of mouse pointer displayed when over part of an object."), helpcontext(0x00033488)] HRESULT MousePointer([out, retval] MousePointerConstants* pMousePointers); [id(0x00000003), propput, helpstring("Returns/sets the type of mouse pointer displayed when over part of an object."), helpcontext(0x00033488)] HRESULT MousePointer([in] MousePointerConstants pMousePointers); [id(0x00000004), propget, helpstring("Sets a custom mouse icon."), helpcontext(0x00033489)] HRESULT MouseIcon([out, retval] IPictureDisp** ppPictureDisp); [id(0x00000004), propputref, helpstring("Sets a custom mouse icon."), helpcontext(0x00033489)] HRESULT MouseIcon([in] IPictureDisp* ppPictureDisp); [id(0x00000004), propput, helpstring("Sets a custom mouse icon."), helpcontext(0x00033489)] HRESULT MouseIcon([in] IPictureDisp* ppPictureDisp); [id(0x00000005), propget, helpstring("Returns or sets a control's current Value property."), helpcontext(0x0003348a)] HRESULT Value([out, retval] float* pfValue); [id(0x00000005), propput, helpstring("Returns or sets a control's current Value property."), helpcontext(0x0003348a)] HRESULT Value([in] float pfValue); [id(0x0000060f), propget, helpstring("Returns/Sets whether this control can act as an OLE drop target."), helpcontext(0x00033692)] HRESULT OLEDropMode([out, retval] OLEDropConstants* psOLEDropMode); [id(0x0000060f), propput, helpstring("Returns/Sets whether this control can act as an OLE drop target."), helpcontext(0x00033692)] HRESULT OLEDropMode([in] OLEDropConstants psOLEDropMode); [id(0xfffffdf8), propget, helpstring("Returns/sets whether or not controls, Forms or an MDIForm are painted at run time with 3-D effects."), helpcontext(0x0003348b)] HRESULT Appearance([out, retval] AppearanceConstants* penumAppearances); [id(0xfffffdf8), propput, helpstring("Returns/sets whether or not controls, Forms or an MDIForm are painted at run time with 3-D effects."), helpcontext(0x0003348b)] HRESULT Appearance([in] AppearanceConstants penumAppearances); [id(0xfffffe08), propget, helpstring("Returns/sets the border style for an object."), helpcontext(0x0003348c)] HRESULT BorderStyle([out, retval] BorderStyleConstants* penumBorderStyles); [id(0xfffffe08), propput, helpstring("Returns/sets the border style for an object."), helpcontext(0x0003348c)] HRESULT BorderStyle([in] BorderStyleConstants penumBorderStyles); [id(0xfffffdfe), propget, helpstring("Returns/sets a value that determines whether a form or control can respond to user-generated events."), helpcontext(0x0003348d)] HRESULT Enabled([out, retval] VARIANT_BOOL* bEnabled); [id(0xfffffdfe), propput, helpstring("Returns/sets a value that determines whether a form or control can respond to user-generated events."), helpcontext(0x0003348d)] HRESULT Enabled([in] VARIANT_BOOL bEnabled); [id(0x00000006), propget, helpstring("Returns/sets a value that determines whether the Progress Bar is displayed vertically or horizontally."), helpcontext(0x0003376d)] HRESULT Orientation([out, retval] OrientationConstants* penumOrientation); [id(0x00000006), propput, helpstring("Returns/sets a value that determines whether the Progress Bar is displayed vertically or horizontally."), helpcontext(0x0003376d)] HRESULT Orientation([in] OrientationConstants penumOrientation); [id(0x00000007), propget, helpstring("Returns/sets a value that determines whether the control displays progress with a standard segmented bar or a smooth bar."), helpcontext(0x0003376e)] HRESULT Scrolling([out, retval] ScrollingConstants* penumScrolling); [id(0x00000007), propput, helpstring("Returns/sets a value that determines whether the control displays progress with a standard segmented bar or a smooth bar."), helpcontext(0x0003376e)] HRESULT Scrolling([in] ScrollingConstants penumScrolling);
Comparing the list of properties with the properties exposed on Application Builder Property Editor I found that all properties except Value
property are available for change at design time.
The Value
property looks like Max
and Min
properties, only the name and id are obviously specific, however we cannot change the Value
property at runtime like we can do for Min
and Max
properties!
The question is: Why Value property is not available for change at design time? What is criteria used by Application Builder to decide not to expose this property at design time?
2. On the next step I tried to find out the structure of the .wrx file binary content and compare with the structure for PSTimer. The binary content for ProgressBar looks like:
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ------------------------------------------------ 00000000: 00 00 00 00 00 00 00 00 64 00 00 00 64 00 00 00 00000010: 26 7B 33 35 30 35 33 41 32 32 2D 38 35 38 39 2D 00000020: 31 31 44 31 2D 42 31 36 41 2D 30 30 43 30 46 30 00000030: 32 38 33 36 32 38 7D 0B 50 72 6F 67 72 65 73 73 00000040: 42 61 72 48 00 00 00 39 00 33 00 36 00 38 00 32 00000050: 00 36 00 35 00 45 00 2D 00 38 00 35 00 46 00 45 00000060: 00 2D 00 31 00 31 00 64 00 31 00 2D 00 38 00 42 00000070: 00 45 00 33 00 2D 00 30 00 30 00 30 00 30 00 46 00000080: 00 38 00 37 00 35 00 34 00 44 00 41 00 31 00 50 00000090: 00 00 00 21 43 34 12 08 00 00 00 56 0A 00 00 56 000000A0: 0A 00 00 01 8A AB 97 00 00 06 00 1C 00 00 00 00 000000B0: 00 00 00 00 00 C8 42 00 00 00 00 01 EF CD AB 00 000000C0: 00 05 00 78 EA 29 00 00 00 00 00 FF FF FF FF FF 000000D0: FF FF FF 84 EA 29 00 1F DE EC BD 01 00 05 00 84 000000E0: EA 29 00 08 00 00 00 0B 50 72 6F 67 72 65 73 73 000000F0: 42 61 72 00 00 FF FF FF FF 03 77 77 77 01 00 00 00000100: 00 00 00 00 00 00 00 00 00
Doing a decoding on that binary data I decomposed the content as follow:
// HEADER 00000000: 00 00 00 00 00 00 00 00 64 00 00 00 64 00 00 00 // ??? Unknown 16 bytes header // CLSID 00000010: 26 // size of CLSID string = 38 00000011: 7B 33 35 30 35 33 41 32 32 2D 38 35 38 39 2D 31 31 44 31 2D 42 31 36 41 2D 30 30 43 30 46 30 32 38 33 36 32 38 7D // CLSID = {35053A22-8589-11D1-B16A-00C0F0283628} // COCLASS:NAME 00000037: 0B // size of string = 11 00000038: 72 6F 67 72 65 73 73 42 61 72 // ProgressBar // LICENSE http://www.dbase.com/KnowledgeBase/dbulletin/bu13_a.htm // https://github.com/atlab/matlab-utils/blob/master/actxlicense.m // 00000043: 48 00 00 00 // size = 72 00000047: 39 00 33 00 36 00 38 00 32 00 36 00 35 00 45 00 2D 00 38 00 35 00 46 00 45 00 2D 00 31 00 31 00 64 00 31 00 2D 00 38 00 42 00 45 00 33 00 2D 00 30 00 30 00 30 00 30 00 46 00 38 00 37 00 35 00 34 00 44 00 41 00 31 00 // license = 9368265E-85FE-11d1-8BE3-0000F8754DA1 // OCX:DATA BEG (80 bytes) 0000008F: 50 00 00 00 // size of OCX:DATA = 80 00000093: 21 43 34 12 08 00 00 00 56 0A 00 00 56 0A 00 00 01 8A AB 97 00 00 06 00 1C 00 00 00 // ??? 000000AF: 00 00 00 00 // Min = 0. (float) 000000B3: 00 00 C8 42 // Max = 100. (float) 000000B7: 01 00 // Orientation = 1. (short) 000000B9: 01 00 // Scrolling =`1. (short) 000000BB: 01 EF CD AB 00 00 05 00 // ??? 000000C3: 78 EA 29 00 // ??? // store many properties values. decoded based on a binary mask. 000000C7: 00 06 // default // BorderStyle mask = 0x0001 // Enabled mask = 0x0002 // Appearance mask = 0x0004 // MousePointer mask = 0x03f8 // OleDropMode mask = 0x2000 // 0x0006 - (0000 0000 0000 0110) default value for: // BorderStyle = 0. // Enabled = True. // Appearance = 1. // MousePointer = 0. // OleDropMode = 0. // 0x231F - (0010 0011 0001 1111) value for: // BorderStyle = 1. // Enabled = True. // Appearance = 1. // MousePointer = 99. // OleDropMode = 1. 000000C9: 00 00 // ??? 000000CB: FF FF FF FF FF FF FF FF // ??? 000000D3: 84 EA 29 00 1F DE EC BD 01 00 05 00 84 EA 29 00 // ??? // OCX:DATA END // FOOTER (0000008F + 0x00000050 + 0x04 = 000000E3) 000000E3: 08 00 00 00 // ??? 000000E7: 0B // size of Name string 000000E8: 50 72 6F 67 72 65 73 73 42 61 72 // Name = ProgressBar. 000000F3 00 00 FF FF FF FF // ??? 000000F9: 00 // Tag 000000FA: 01 00 00 00 // HonorProKeys = True. 000000FE: 00 00 00 00 // HonorReturnKey = False. 00000102: 01 00 00 00 // Visible = True.The general structure of data looks to have the same pattern like on PStimer. Let explain:
- HEADER - is a 16 bytes array which I don't know how should be interpreted!
- CL1SID - is a UTF-8 string containing the CLSID for ProgressBar.
- COCLASS:NAME - Is the name of COCLASS, not to be confused with the name of the control instance.
- LICENSE - This is something new that I found searching on the internet for UUID value. Yey, now we know that this field contains the OCX LICENSE. (http://www.dbase.com/KnowledgeBase/dbulletin/bu13_a.htm) On old OCX's could be a string instead of a UUID but this is not relevant.
- OCX:DATA - Here, is where the OCX properties are stored. The first 4 bytes is the size of this block. Unfortunately there are a lot of fields which I cannot explain what they are meaning.
In order to find out how and where property values are stored I used Property Editor to change properties one by one, then save and compare the result.
Using this "hand" technique for ProgressBar I found:
-Min
andMax
are stored asfloat
values.
-Orientation
andScrolling
are enum values and according to MS IDL specifications (https://docs.microsoft.com/en-us/windows/win32/midl/midl-start-page), are short values by default.
What's coming next is really difficult to understand and find the logic behind:
-BorderStyle
,Enabled
,Appearance
,MousePointer
andOleDropMode
values are stored in s single short value! All areenum
types exceptEnabled
which is a boolean value.
In order to decode these values we have to use a bit masks as I observed during my tests!
Those properties can be changed from custom properties dialog. Min, Max and Orientation, also could be changed from custom properties dialog but are stored separate not combined!
Why Application Builder combine those values? Which criteria is used to decide how to combine values and for which properties?
For exampleOrientation
andScrolling
are alsoenum
types having two possible values, 0 or 1, but they are stored separately!
is defined as
@EnabledVARIANT_BOOL
(https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/7b39eb24-9d39-498a-bcd8-75c38e5823d0) having 2 possible values
0xFFFF (TRUE) or 0x0000 (FALSE).VT_BOOL
is also similar defined (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-mqcn/085232f8-e4f3-4dcc-9150-df57c8bdbbfa)
Looking at note #4438-10 for PSTimer theVARIANT_BOOL
value is stored in 1 byte and in ProgressBar in 1 bit!
- FOOTER - Is the last structure in the .wrx file. Even though here we have also unknown data, getting the
Name
of the control is not a problem. He is always a string at offset 0x0004 from
the beginning of FOOTER.
.wrx
binary content:
- The structure pattern is the same, and seems to be always the same, a serie of structures: HEADER, CLSID, COCLASS:NAME, LICENSE, OCX:DATA and FOOTER.
- It's easy to traverse and extract data for all these structures except for
OCX:DATA
which is the most complex structure and looks different for each OCX.
Unfortunately it's impossible to find a logic behind the structure of OCX:DATA block! There are a lot of unknown data in this block which make impossible to "guess" the real structure.
#15 Updated by Greg Shah over 4 years ago
Why Value property is not available for change at design time? What is criteria used by Application Builder to decide not to expose this property at design time?
I think Value
is treated as a special property. From IDispatch::GetIDsOfNames():
An IDispatch implementation can associate any positive integer ID value with a given name. Zero is reserved for the default, or Value property; –1 is reserved to indicate an unknown name; and other negative values are defined for other purposes.
The 4GL may simply only deal with non-zero property ids. On the other hand, the type library tool reports Value as property id 5 so I don't know. Perhaps the properties with the name "Value" are also excluded as special (not to be set at design time).
It's easy to traverse and extract data for all these structures except for OCX:DATA which is the most complex structure and looks different for each OCX.
Unfortunately it's impossible to find a logic behind the structure of OCX:DATA block! There are a lot of unknown data in this block which make impossible to "guess" the real structure.
I wonder if the 1C 00 00 00
at AB
is the length of the property data (from AF
through CA
), just before the FFFFFFFFFFFFFFFF
at CB
. Then again, this doesn't appear in the PSTimer data so it is probably just me imagining patterns where none exist.
I guess one of the following is true:
- The structure of the OCX:DATA section can be deduced from the property type info that can be inspected via the COM APIs; OR
- The structure is encoded inside the
.wrx
itself. The only obvious possibilities of this is the data from93
throughAA
and/orBB
throughC6
and/or the data fromD3
throughE6
. Everything else seems to be accounted for.
Considering that the property ids are not encoded in the data and both the size and order of the OCX:DATA block is fixed, I suspect it can be deduced from the exposed COM API (e.g. inspecting the type library).
Why Application Builder combine those values? Which criteria is used to decide how to combine values and for which properties?
Are you sure that the Enabled
property is stored in that same bit field? All the other values stored there are part of the same set of MS common control constants. In other words, the type library for the common controls probably exports this as a single bit field that has multiple enums defined as "sections". I suspect that the masks and encoding map directly to how the type library reports the properties structure.
As a next step, please dig in to the type library APIs (the ITypeLib
and ITypeInfo
seem most useful). Writing some code to use these APIs may explain some things that are otherwise quite strange right now.
#16 Updated by Vladimir Tsichevski over 4 years ago
Greg Shah wrote:
Why Value property is not available for change at design time? What is criteria used by Application Builder to decide not to expose this property at design time?
The following is an excerpt from here: https://documentation.progress.com/output/ua/OpenEdge_latest/dvpin/understanding-design-time-and-runtime-properties.html
Understanding design time and runtime properties
To customize the definition for an ActiveX control, you must change the values of control properties in the AppBuilder at design time. Each ActiveX control supports a specific list of properties that you can set at design time (design time properties) and another list of properties that you can read or write at runtime (run-time properties). (You can set some properties at both design time and runtime.)
To modify design time properties, you generally require a license from the control vendor. The vendor typically provides this license to application developers (as opposed to application end-users) for installation with the control.
The AppBuilder provides access to all available design time properties using the OCX Property Editor window. This window contains all design time properties including the extended properties that ABL adds. This is the only way to set values for properties writable only at design time.
To modify run-time properties, you write ABL code that sets the property value using the component handle to the control.
The Value
property of ProgressBar
is a runtime property, you cannot set it neither in AppBuilder not it the "native" property editor in VisualStudio.
#17 Updated by Greg Shah over 4 years ago
The appbuilder code (see possenet in the src/adeuib/
directory) itself uses an OCX control for the "Property Editor" (and probably for some other parts too). I wonder if this OCX is from Microsoft. Anyway, the reliance upon external native code may be one reason for the appbuilder to be quite unstable.
#18 Updated by Greg Shah over 4 years ago
- Related to Feature #4174: implement calendar control/dtpicker OCX replacement added
#21 Updated by Greg Shah over 3 years ago
A very short knowledge base article asks:
What do wrx files contain?
Here is the documented answer:
WRX files contain design and runtime license keys (if required by the ActiveX Controls) along with any custom property settings that were made to any ActiveX Control in the window.
WRX files do not contain any ABL source or .r code.
#22 Updated by Hynek Cihlar over 2 years ago
Greg Shah wrote:
The appbuilder code (see possenet in the
src/adeuib/
directory) itself uses an OCX control for the "Property Editor" (and probably for some other parts too).
Can't we use the OCX to parse WRX files?
#23 Updated by Vladimir Tsichevski over 2 years ago
Hynek Cihlar wrote:
Greg Shah wrote:
The appbuilder code (see possenet in the
src/adeuib/
directory) itself uses an OCX control for the "Property Editor" (and probably for some other parts too).Can't we use the OCX to parse the WRX files?
WRX files are from the OpenEdge domain, OCX controls know nothing about the WRX.
We can probably extract the data from a WRX file using the following steps:
- In AppBuilder, create a control for the same type of OCX, save the control along with a WRX file.
- Replace the WRX file by one we want to extract data from
- Re-open the control with AppBuilder, see the property values.
#24 Updated by Hynek Cihlar over 2 years ago
Vladimir Tsichevski wrote:
Hynek Cihlar wrote:
Greg Shah wrote:
The appbuilder code (see possenet in the
src/adeuib/
directory) itself uses an OCX control for the "Property Editor" (and probably for some other parts too).Can't we use the OCX to parse the WRX files?
WRX files are from the OpenEdge domain, OCX controls know nothing about the WRX.
I meant the specific OCX control Greg mentions above. If it is used for showing/editing OCX properties, we could use it, too.
#25 Updated by Vladimir Tsichevski over 2 years ago
Hynek Cihlar wrote:
Vladimir Tsichevski wrote:
Hynek Cihlar wrote:
Greg Shah wrote:
The appbuilder code (see possenet in the
src/adeuib/
directory) itself uses an OCX control for the "Property Editor" (and probably for some other parts too).Can't we use the OCX to parse the WRX files?
WRX files are from the OpenEdge domain, OCX controls know nothing about the WRX.
I meant the specific OCX control Greg mentions above. If it is used for showing/editing OCX properties, we could use it, too.
Which exactly OCX control is it?
#26 Updated by Hynek Cihlar over 2 years ago
Vladimir Tsichevski wrote:
Which exactly OCX control is it?
See Greg's note #4438-17. I just had a quick look in the possenet sources so I can't give you good answer ATM.
#27 Updated by Vladimir Tsichevski over 2 years ago
Hynek Cihlar wrote:
Vladimir Tsichevski wrote:
Which exactly OCX control is it?
See Greg's note #4438-17. I just had a quick look in the possenet sources so I can't give you good answer ATM.
If this OCX is from Microsoft, when it cannot be used to read WRX files.
#28 Updated by Hynek Cihlar over 2 years ago
The OCX properties are indeed handled by a COM component, but this comes with OpenEdge. It resides in prox.dll
and the following is its type library.
Unfortunately it doesn't contain methods for manipulating individual OCX properties. The main method is EditProperties
, it handles all the edit flow - UI and persistence.
When looking at the imported system functions of prox.dll
the following few look interesting:
StgCreateDocfile StgOpenStorage CreateILockBytesOnHGlobal StgCreateDocfileOnILockBytes StgOpenStorageOnILockBytes
Apparently it uses COM compound storage for storing the properties (or perhaps only their related data). StgCreateDocfileOnILockBytes
creates compound storage on a provided byte buffer, perhaps prox.dll
embeds compound storage in wrx files? I.e. the wrx itself is proprietary but part of it is the standard COM compound storage?
#29 Updated by Hynek Cihlar over 2 years ago
Here are some more findings.
Every wrx file is a COM/OLE structured storage file so it can be treated with the COM/OLE Windows system functions like StgOpenStorage
.
The structured storage format is a directory like structure. The data is stored in streams (represented by IStream
COM interface), streams are placed in directories called storages (represented by IStrogare
COM interface). Storages can be nested. Structured storage file can also contain property sets, which always occur at the top level and can't be nested. Property is a name-value structure.
The WRX file contains n number of root storages for every control frame. The storage is named after the control frame's NAME
property value. Every control frame storage contains two streams, one named Frame
and the other Control0
. The WRX file at its root level doesn't contain any property sets.
Now the content of the streams is what's unknown. It is either completely proprietary or it also contains a structured storage blob embedded. I will do some quick tests tomorrow to confirm this.
I created a utility for dumping data of all the streams in a wrx file. I will post this tomorrow.
#30 Updated by Hynek Cihlar over 2 years ago
I have some more findings.
Please note that all the presented results are preliminary as they were tested on a single OCX object. It is possible that different OCX object capabilities (like the supported standard OLE interfaces) may make prox.dll
choose different approach how it implements persistence of OCX properties.
When LoadControls
is invoked the following happens:
1. prox.dll
opens the corresponding wrx and reads the OCX Object IDs.
2. prox.dll
uses the Object IDs and loads the type library of the OCXes being loaded, it introspects the OCX and gathers all the design properties, their names, IDs, enum labels and enum values, etc...
3. prox.dll
instantiates the OCX object using the IDs loaded from Control0
stream.
4. prox.dll
queries IPersistMemory
and IPersistStreamInit
interfaces from the instantiated OCX. It uses them (or one of them, not yet sure) to load the data from the Control0
stream (or part of it). Hence the property data format we see, if this will be confirmed with further tests, is proprietary to every OCX.
5. prox.dll
queries the value of every design property of the OCX previously loaded with the stream data and displays it in the property grid.
We could create a utility that would essentially do the same, but instead of showing the property values in a grid we would output them in a predefined (xml) format in a file and use it during conversion (or runtime if LoadControls
is preserved after conversion).
Unknowns:
1. Does prox.dll
use all the stream data from the structured storage IStream
for deserializing the props or does it use a subset?
2. Does the specific OCX have to support all the OLE COM interfaces mentioned above? How will prox.dll
behave when some of the key interfaces are not supported by the OCX? Is this even an issue? Do all OCX's we currently convert support all the above mentioned OLE interfaces?
3. How does prox.dll
select the design OCX properties?
4. There seem to be some 4GL specific properties shown in the prox.dll
property editor, like Visible
or Enabled
. If needed these will have to be handled with a different logic.
My next step will be to write the utility to dump the property values and try it on the other OCXes.
#32 Updated by Hynek Cihlar about 2 years ago
I can report a success in parsing OCX properties for PSTimer. I successfully read the Interval and Enabled properties.
But there is still some work required. Especially in deserializing all the various property data types and confirming some of the assumptions (like the offset in the IStream stream where the actual OCX data start).
I will now focus on more complex OCXes and on adding the missing pieces for the proper parser.
#33 Updated by Greg Shah about 2 years ago
That is huge! It will be a big step ahead since we will be able capture the output for all .wrx
files up front.
I assume the utility being developed compiles and links using the mingw approach we have in our src/native/
. I'm looking forward to seeing the code. What are your thoughts on how to encode the output (e.g. XML)?
#34 Updated by Hynek Cihlar about 2 years ago
Greg Shah wrote:
That is huge! It will be a big step ahead since we will be able capture the output for all
.wrx
files up front.I assume the utility being developed compiles and links using the mingw approach we have in our
src/native/
.
I'm currently compiling the code in Visual Studio, but I plan to convert the project into mingw.
I'm looking forward to seeing the code.
At this point it is very prototypal.
What are your thoughts on how to encode the output (e.g. XML)?
Yes, I think an XML processed during conversion should work. The structure could look like the one below.
<wrx-properties> <object id="<an identifier (clsid?)>"> <property name="" type="" value=""/> <property name="" type="" value=""/> <property name="" type="" value=""/> ... </object> <object id="<an identifier (clsid?)>"> <property name="" type="" value=""/> <property name="" type="" value=""/> <property name="" type="" value=""/> ... </object> ... </wrx-properties>
#35 Updated by Greg Shah about 2 years ago
It all looks good.
#36 Updated by Hynek Cihlar about 2 years ago
Some of the OCXes require a license key otherwise they fail to instantiate. I found out the license key is part of the wrx file. However I don't know yet how to calculate its offset.
#37 Updated by Hynek Cihlar about 2 years ago
Hynek Cihlar wrote:
Some of the OCXes require a license key otherwise they fail to instantiate. I found out the license key is part of the wrx file. However I don't know yet how to calculate its offset.
Thanks Marius for figuring this out in note #4438-10 :-).
#38 Updated by Hynek Cihlar about 2 years ago
- % Done changed from 0 to 90
The initial version of Wrx parser is finished. I can already use it to unblock some of the related issues.
The remaining tasks:
1. Parsing some wrx files gives errors. I think the problem is in the wrong object state data offset in the wrx file.
2. The sources must be currently built with Visual Studio until it is ported in MinGW.
3. Some code beautification.
Greg, where can I commit the sources? You mentioned src/native
, but wouldn't tools
be a better location? I think it is more in line with things like gen-text-metrics.p
.
#39 Updated by Greg Shah about 2 years ago
2. The sources must be currently built with Visual Studio until it is ported in MinGW.
Eugenie can help with this.
Greg, where can I commit the sources? You mentioned
src/native
, but wouldn'ttools
be a better location? I think it is more in line with things likegen-text-metrics.p
.
You aren't wrong that it does fit in tools
but there is a benefit to making it easy to build this. If we put it in tools
, then I think we should have a makefile
. Putting it in src/native/
allows us to leverage that build environment.
#40 Updated by Hynek Cihlar about 2 years ago
Greg Shah wrote:
2. The sources must be currently built with Visual Studio until it is ported in MinGW.
Eugenie can help with this.
Greg, where can I commit the sources? You mentioned
src/native
, but wouldn'ttools
be a better location? I think it is more in line with things likegen-text-metrics.p
.You aren't wrong that it does fit in
tools
but there is a benefit to making it easy to build this. If we put it intools
, then I think we should have amakefile
. Putting it insrc/native/
allows us to leverage that build environment.
OK I checked the source file in src/native
. I will provide the makefile later. See 3821c revision 13540.