I need to request and retrieve an image of a window from the X server or the WM (I believe the WM does the actual compositing). I need to be able to get an image of the window even if it is obscured by another window or located on another workspace (but still mapped). I need to use C or C++ and would like to use XCB or Xlib. I think I also have Gtk+ development tools installed but it's been a while since I used them.
I have tried using xcb_composite_name_window_pixmap() with no success (XCB errors).
I tried using the render extension to render the window to a picture which produced no errors, but I still needed to get the image loaded into my program's memory.
I tried to use xcb_get_image to get the picture which, IIRC, failed (bad resource I believe).
I tried to copy the picture to a pixmap and use xcb_get_image on that. I was able to download what appeared to be an image, but it looked more like random regions of the screen than the actual window.
In short, I'm just making guesses at this point.
I'm having a difficult time finding organized and complete documentation. The most recent publications I can find on X11, XCB, or Xlib is from 1994 (The O'Reilly book on Xlib and/or X11R6) which I doubt has much, if anything, valid on these extensions. Most of the man pages and online documentation has a lot of "TODO: explain this" and/or function descriptions like "deliver a request to the X server". Any help that anyone can provide will be of use to me.
I'm currently running compiz for my WM and emerald for window decorations and nothing else standard in terms of a "desktop environment". I'm working on some utility applications for a custom desktop that I plan to release when they are ready. I would like whatever I make to work with some other WMs but, if it requires special codepaths for each, I can add the others down the line.
EDIT: I originally had added a non-working example here, and then a stripped down working example, which has all been moved to an answer as suggested in the comments.
I now have a working example that I will post here:
#include <xcb/xcb.h>
#include <xcb/xproto.h>
#include <xcb/composite.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "usage: %s windowId\n", argv[0]);
return EXIT_FAILURE;
}
xcb_window_t req_win_id = strtoul(argv[1], NULL, 0);
xcb_connection_t *connection = xcb_connect(NULL, NULL);
xcb_generic_error_t *err = NULL, *err2 = NULL;
xcb_composite_query_version_cookie_t comp_ver_cookie = xcb_composite_query_version(connection, 0, 2);
xcb_composite_query_version_reply_t *comp_ver_reply = xcb_composite_query_version_reply(connection, comp_ver_cookie, &err);
if (comp_ver_reply)
{
if (comp_ver_reply->minor_version < 2) {
fprintf(stderr, "query composite failure: server returned v%d.%d\n", comp_ver_reply->major_version, comp_ver_reply->minor_version);
free(comp_ver_reply);
return EXIT_FAILURE;
}
free(comp_ver_reply);
}
else if (err)
{
fprintf(stderr, "xcb error: %d\n", err->error_code);
free(err);
return EXIT_FAILURE;
}
const xcb_setup_t *setup = xcb_get_setup(connection);
xcb_screen_iterator_t screen_iter = xcb_setup_roots_iterator(setup);
xcb_screen_t *screen = screen_iter.data;
// request redirection of window
xcb_composite_redirect_window(connection, req_win_id, XCB_COMPOSITE_REDIRECT_AUTOMATIC);
int win_h, win_w, win_d;
xcb_get_geometry_cookie_t gg_cookie = xcb_get_geometry(connection, req_win_id);
xcb_get_geometry_reply_t *gg_reply = xcb_get_geometry_reply(connection, gg_cookie, &err);
if (gg_reply) {
win_w = gg_reply->width;
win_h = gg_reply->height;
win_d = gg_reply->depth;
free(gg_reply);
} else {
if (err) {
fprintf(stderr, "get geometry: XCB error %d\n", err->error_code);
free(err);
}
return EXIT_FAILURE;
}
// create a pixmap
xcb_pixmap_t win_pixmap = xcb_generate_id(connection);
xcb_composite_name_window_pixmap(connection, req_win_id, win_pixmap);
// get the image
xcb_get_image_cookie_t gi_cookie = xcb_get_image(connection, XCB_IMAGE_FORMAT_Z_PIXMAP, win_pixmap, 0, 0, win_w, win_h, (uint32_t)(~0UL));
xcb_get_image_reply_t *gi_reply = xcb_get_image_reply(connection, gi_cookie, &err);
if (gi_reply) {
int data_len = xcb_get_image_data_length(gi_reply);
fprintf(stderr, "data_len = %d\n", data_len);
fprintf(stderr, "visual = %u\n", gi_reply->visual);
fprintf(stderr, "depth = %u\n", gi_reply->depth);
fprintf(stderr, "size = %dx%d\n", win_w, win_h);
uint8_t *data = xcb_get_image_data(gi_reply);
fwrite(data, data_len, 1, stdout);
free(gi_reply);
}
return EXIT_SUCCESS;
}
It was a case of overdoing it. I got the idea that I should use Xrender from somewhere and started pulling tons of data from that extension that I didn't really need. But the real problem I had was that the pixmap given to xcb_composite_name_window_pixmap should not be created first, but rather I just needed the new id for it. It turns out that it is really as simple as I expected it to be. All you need is the Window ID and the size, do some checks to make sure composite is there and a good version (>=0.2 in this case), and call xcb_composite_redirect_window, xcb_composite_name_window_pixmap (with a generated pixmap id), and xcb_get_image (+reply,data_len,data) in that order, and you have the image.
Related
I'm trying to use the Leadtools API version 21 for automatically scanning some documents while setting some properties from code (do not want to show the TWAIN dialog). for example I set the scan DPI to 300 using L_TwainSetResolution(), but the image I get inside the bitmap callback always has resolution of 96x96.Here is a sudo code of what I have done (it runs in a secondary thread and the unlock has been done in the main thread):
void CheckRetCode(int rc)
{
if (SUCCESS != rc)
{
L_TCHAR errMsg[1024];
memset(errMsg, 0, sizeof(errMsg));
L_GetFriendlyErrorMessage(rc, errMsg, 1024, L_FALSE);
throw TLeadException(errMsg, rc);
}
}
L_INT EXT_CALLBACK GetBmpCB(HTWAINSESSION hS, pBITMAPHANDLE pBitmap, L_VOID* pUserData)
{
// in here pBitmap->XResolution and pBitmap->YResolution are always 96
// but I have clearly set them to 300
// process image here
L_FreeBitmap(pBitmap); // free the image
return SUCCESS;
}
void OnThreadExecute(void)
{
HTWAINSESSION hSession = nullptr;
APPLICATIONDATA appData;
L_INT nRet;
L_TCHAR pszTwnSourceName[1024];
LTWAINSOURCE sInfo;
TW_FIX32 XRes = L_TwainFloatToFix32(300.0);
TW_FIX32 YRes = L_TwainFloatToFix32(300.0);
BITMAPHANDLE tBmp;
memset(&tBmp, 0, sizeof(BITMAPHANDLE));
tBmp.uStructSize = sizeof(BITMAPHANDLE);
memset(&appData, 0, sizeof(APPLICATIONDATA));
appData.uStructSize = sizeof(APPLICATIONDATA);
appData.hWnd = hWnd;// hWnd is valid handle of my main window
appData.uLanguage = TWLG_ENGLISH_USA;
appData.uCountry = TWCY_USA;
wcscpy(appData.szManufacturerName, L"MyCompanyName");
wcscpy(appData.szAppProductFamily, L"MyProductName");
wcscpy(appData.szAppName, appData.szAppProductFamily);
wcscpy(appData.szVersionInfo, L"Version 0.1.0.1");
nRet = L_TwainInitSession2(&hSession, &appData, LTWAIN_INIT_MULTI_THREADED);
CheckRetCode(nRet);here
memset(pszTwnSourceName, 0, sizeof(pszTwnSourceName));
wcscpy(pszTwnSourceName, L"EPSON Artisan837/PX830");
sInfo.uStructSize = sizeof(LTWAINSOURCE);
sInfo.pszTwainSourceName = pszTwnSourceName;
CheckRetCode(L_TwainSelectSource(hSession, &sInfo));
CheckRetCode(L_TwainStartCapsNeg(hSession));
CheckRetCode(L_TwainSetImageUnit(hSession, TWUN_INCHES));
CheckRetCode(L_TwainEnableDuplex(hSession, FALSE));
CheckRetCode(L_TwainSetResolution(hSession, &XRes, &YRes)); // setting the res to 300 x 300
CheckRetCode(L_TwainEndCapsNeg(hSession));
L_TwainAcquire(hSession, &tBmp, sizeof(BITMAPHANDLE), GetBmpCB, 0, NULL, NULL);
if(tBmp.Flags.Allocated)
L_FreeBitmap(&tBmp);
}
By the way the scanned image has the correct number of pixels. If I scan a 8.5x11 page, I get an image that is 2550x3300 pixels, but XResolution and YResolution are set to 96 which causes the saved image to be 26.5"x34.375".
Thank you
Sam
I tested with 4 different Twain drivers and got the following results:
One of them did not support 300 DPI, so it returned error “Bad value” when calling L_TwainSetResolution(). However, it returned correct image size for the actual DPI it supported, which is 100.
The other 3 supported different DPI values, and returned correct image size and DPI values in the callback’s pBitmap.
The only major difference between my code and yours is that I called L_TwainEndSession(). If your code doesn’t include it, make sure to call it once for each call of L_TwainInitSession/L_TwainInitSession2.
If that’s not the cause of your problem, try to test with more than one Twain driver and see if the problem is specific to one driver. If it’s not, put your code in a small test program and email it to support#leadtools.com and we will check it for you.
I'm trying to get the "friendly name" of a plugged-in USB device. I'm using SetupDiGetDeviceRegistryProperty method with SPDRP_FRIENDLYNAME property but the method returns false and sets the error code to ERROR_INVALID_DATA, although everything works fine with other properties, such as SPDRP_DEVICEDESC or SPDRP_MFG.
I checked the registry and the Device Manager and the friendly name exists.
Does anyone have any idea?
UPDATE: What i tried so far:
GUID hidGuid;
HidD_GetHidGuid(&hidGuid);
HDEVINFO hDevInfo = SetupDiGetClassDevs(&hidGuid, 0, 0, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
if (INVALID_HANDLE_VALUE == hDevInfo)
{
AfxMessageBox(CString("SetupDiGetClassDevs(): ")
+ _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
return;
}
SP_DEVINFO_DATA* pspDevInfoData =
(SP_DEVINFO_DATA*)HeapAlloc(GetProcessHeap(), 0, sizeof(SP_DEVINFO_DATA));
pspDevInfoData->cbSize = sizeof(SP_DEVINFO_DATA);
for (int i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, pspDevInfoData); i++)
{
DWORD DataT;
DWORD nSize = 0;
TCHAR buf[MAX_PATH];
if (!SetupDiGetDeviceInstanceId(hDevInfo, pspDevInfoData, buf, sizeof(buf), &nSize))
{
AfxMessageBox(CString("SetupDiGetDeviceInstanceId(): ")
+ _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
break;
}
if (SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData,
SPDRP_FRIENDLYNAME, &DataT, (PBYTE)buf, sizeof(buf), &nSize))
{
//display buf
}
else
{
if (GetLastError() == ERROR_INVALID_DATA)
{
//display ERROR_INVALID_DATA
}
if (SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData, SPDRP_MFG, &DataT, (PBYTE)buf, sizeof(buf), &nSize))
{
//display buf
}
if (SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData,
SPDRP_DEVICEDESC, &DataT, (PBYTE)buf, sizeof(buf), &nSize))
{
// display buf
}
}
}
Something like this. As i said, i get the device description and the device manufacturer, but not the friendly name.
Not all devices have SPDRP_FRIENDLYNAME attribute set. When that's the case, ERROR_INVALID_DATA is expected, it tells you just that.
When they don’t have it, device manager GUI uses another one for display name, SPDRP_DEVICEDESC
Maybe useful information:
In my case I had two network adapters, but the function succeeded only for the adapter that is shown as "... #2" in device manager when SPDRP_FRIENDLYNAME is used.
I could also verify that the other adapter (without the "... #2") does not have a value "FriendlyName" in it's registry data.
This behaviour seems to depend on the O/S. In may case the funtion succeeded always when SPDRP_FRIENDLYNAME is used Windows 10, but only worked for the device that was shown as "...#2" in device manager.
This issue mainly goes down to Windows 10 stopping unsigned drivers from installing, even when the 'driver' is just a .inf file that simply references a (presumably signed) windows DLL, but is there to change the "USB Serial Device" into something meaningful to humans, and recognisable by the application software. I've had to re-write 10 different projects because of this issue.
I now have to check for specific VID/PID, however it is not future proof.
I am trying to temporarily install a font to use in the win32 console with
int AddFontResource(LPCTSTR lpszFilename);
and
BOOL WINAPI SetConsoleFont(HANDLE hOutput, DWORD fontIndex)
I got hold of this function from this site.
Although both functions seem to work fine I have no idea how to find the added font index to use with SetConsoleFont.
AddFontResource returns no index value or key to the temporary font.
Here is my relevant code:
#include "Level.h"
#include "ConsoleFont.h" //acquired from above mentioned site
#include <Windows.h>
//-------------------------------------------------------------------------------
void init();
void cleanup();
int main()
{
FileManager *pFileManager = new FileManager(); //unrelated
Level *lvl1 = new Level("filename",pFileManager); //unrelated
///TEMPORARY PLANNING
// using add font resource. how can i get this fonts index value?
int err = AddFontResource(L"Files/gamefont.fnt");
if (err == 0)
{
MessageBox(NULL,L"loading font failed",L"Error",0);
}
else
{
wchar_t message[100];
swprintf_s(message,100,L"AddFontResource returned: %d",err);
MessageBox(NULL,LPTSTR(message),L"error",0);
}
SendMessage(HWND_BROADCAST, WM_FONTCHANGE,0,0);
//acquiring handle to current active screen buffer
HANDLE tempHandle = GetStdHandle(STD_OUTPUT_HANDLE);
if (tempHandle == INVALID_HANDLE_VALUE)
{
MessageBox(NULL,L"Failed to aquire Screen Buffer handle",L"Error",0);
}
//I dont know what to set this to. this is the crux of the problem.
DWORD fontIndex = 1;
if (FALSE == SetConsoleFont(tempHandle,fontIndex))
{
MessageBox(NULL,L"loading console font failed",L"Error",0);
}
//draws a house when in correct font
std::cout<<"!!!!!!!!#\n"
<<"!!!!!!!!!\n"
<<"! !! !! !\n"
<<"!!!!!!!!!\n"
<<"! !! !! !\n"
<<"!!!!!!!!!\n"
<<"! !! !! !\n"
<<"!!!!!!!!!\n"
<<"! !! !! !#\n"
<<"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"<<std::endl;
///PLANNING OVERS
bool quit = false;
while(!quit)
{
//still to be implemented
}
err = RemoveFontResource(L"Files/gamefont.fnt");
if (err==0)
{
MessageBox(NULL,L"removing font failed",L"Error",0);
}
return 0;
}
I don't know how to go about finding my new font's index value or even if this is possible using my current method.
If someone knows or has a better method please help me out.
any help or hints are appreciated. It must possible to use a custom font in the win32 Console without fiddling with the registry. I'm sure of it :)
Unfortunately you entered the dark world on Win APIs. There is no documentation (or atleast I could never find it) for a console font table lookup. You can try the method "GetNumberOfConsoleFonts()" to see what is returned. I think the font at index 10 is Lucida Console. You'll have to play around a little. Also, this may not work for the OS version you have. Worked for me on XP. Never had to try on anything else. And honestly, never got it fully working on XP too.
For the registry,
Fonts registries are here:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts
Console registries are here:
HKEY_CURRENT_USER\Console
If you end up modifying the registry, the changes may not be reflected immediately. You need to either restart the console or send a special WM_* message (sorry don't remember the name).
Will be great if you can find a solution/workaround :)
int err = AddFontResource(L"Files/gamefont.fnt");
if (err == 0)
{
MessageBox(NULL,L"loading font failed",L"Error",0);
}
else
{
wchar_t message[100];
swprintf_s(message,100,L"AddFontResource returned: %d",err);
MessageBox(NULL,LPTSTR(message),L"error",0);
}
this is wrong AddFontResource returns the number of fonts loaded, so the code in the ELSE doesn't make sense.
I just asked a question earlier today because I wanted to run an executable file that takes parameters from my C++ code and it wasn't working.
It works now, but I'm still having problems since I thought I was going the right way about this, but it seems like what I want to accomplish can't be done the way I'm approaching it...
This is my corrected code from my other question:
#include <stdlib.h>
#include <conio.h>
int main (){
system("\"\"C:\\Users\\Adam\\Desktop\\pdftotext\" -layout \"C:\\Users\\Adam\\Desktop\\week 4.pdf\"\"");
_getch();
}
which is me running "pdftotext -layout myfile.pdf" as if I was running it from a CMD window.
The thing is, I don't actually want the cmd to show up since I have a GUI interface on top of it and I want to display a nicer progress bar instead of seeing the windows pop-up for every file I need to parse.
I looked around and either I don't understand what I'm reading since I'm relatively new to C++, or I just didn't find what I was looking for. I found that using CreateProcess, I should be able to do this, but after copying some code I found somewhere else, the cmd window pops-up anyway.
I'd like it if someone could give me the name of a function I could use to accomplish something like this or if someone could give some example code for this small case in the code I posted since I'm not sure I understand everything as I should, being new to C++ and all.
Edit: As requested in a comment, the code for CreateProcess that I tried is what I found at this url:
http://www.goffconcepts.com/techarticles/development/cpp/createprocess.html
Which is (with my own parameters that I think should go there):
#include <windows.h>
#include <string>
#include <conio.h>
size_t ExecuteProcess(std::wstring FullPathToExe, std::wstring Parameters, size_t SecondsToWait)
{
size_t iMyCounter = 0, iReturnVal = 0, iPos = 0;
DWORD dwExitCode = 0;
std::wstring sTempStr = L"";
/* - NOTE - You should check here to see if the exe even exists */
/* Add a space to the beginning of the Parameters */
if (Parameters.size() != 0)
{
if (Parameters[0] != L' ')
{
Parameters.insert(0,L" ");
}
}
/* The first parameter needs to be the exe itself */
sTempStr = FullPathToExe;
iPos = sTempStr.find_last_of(L"\\");
sTempStr.erase(0, iPos +1);
Parameters = sTempStr.append(Parameters);
/* CreateProcessW can modify Parameters thus we allocate needed memory */
wchar_t * pwszParam = new wchar_t[Parameters.size() + 1];
if (pwszParam == 0)
{
return 1;
}
const wchar_t* pchrTemp = Parameters.c_str();
wcscpy_s(pwszParam, Parameters.size() + 1, pchrTemp);
/* CreateProcess API initialization */
STARTUPINFOW siStartupInfo;
PROCESS_INFORMATION piProcessInfo;
memset(&siStartupInfo, 0, sizeof(siStartupInfo));
memset(&piProcessInfo, 0, sizeof(piProcessInfo));
siStartupInfo.cb = sizeof(siStartupInfo);
if (CreateProcessW(const_cast<LPCWSTR>(FullPathToExe.c_str()),
pwszParam, 0, 0, false,
CREATE_DEFAULT_ERROR_MODE, 0, 0,
&siStartupInfo, &piProcessInfo) != false)
{
/* Watch the process. */
dwExitCode = WaitForSingleObject(piProcessInfo.hProcess, (SecondsToWait * 1000));
}
else
{
/* CreateProcess failed */
iReturnVal = GetLastError();
}
/* Free memory */
delete[]pwszParam;
pwszParam = 0;
/* Release handles */
CloseHandle(piProcessInfo.hProcess);
CloseHandle(piProcessInfo.hThread);
return iReturnVal;
}
int main(void){
ExecuteProcess(L"C:\\Users\\Adam\\Desktop\\pdftotext", L"-layout \"C:\\Users\\Adam\\Desktop\\week 4.pdf\"", 0);
_getch();
}
I'm a little bit overwhelmed since it uses some things I've never used before, but I think I understand the core (keyword: think). It doesn't solve my problem, though, because the cmd shows up and by retesting it I actually noticed that the cmd doesn't even run the .exe and doesn't give me an error message.
I hope this bit of code helps... I didn't want to look into it further since it seemed like I wasn't even going in the right direction.
Use CreateProcess instead of system.
--EDIT--
the code for CreateProcess that I tried is what I found at this url:
The code is a mess, I'd advise to avoid that url in future.
At the end of "CreateProcess" article is a link named "Creating Processes", which contains simpler example that is easier to read. Use it as a starting point.
After adding the following lines for siStartupInfo, cmd window won't pop up any more with my own test *.exe.
siStartupInfo.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
siStartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
siStartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
siStartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
siStartupInfo.wShowWindow = SW_HIDE;
But I have another problem. As I try to run some other executable, whose command line would be
TEST.exe <input-file> output-file
in a cmd window, WaitForSingleObject() return 258, and GetLastError() return 1813 ("The specified resource type cannot be found in the image file.").
See system() and CreateProcess() / CreateProcessW() for more details.
Any ideas would be highly appreciated!
The only way I found how to execute an external program is:
system("start C:\file path\ file");
The only problem is that the file or directory can't have spaces.
I am working on getting the version of the Software which is installed on the Computer. I have implemented the logic for reading the Uninstall hive of registry, but i have observed that some of the software are not having version entries in the Uninstall hive of the registry. But i want to show the version of those softwares also.
Can some one help me out in this regard?
Supplying a software version to the registry of Windows is voluntary. If the developer of the software you're looking at chose to not display the version there or was simply unaware of such possibility, I am unable to point you to any other location he would choose to use or be aware of. In fact, the software might not even have a version number/name.
Ask yourself this: Where else is the Version detail of the software available if not in the registry? If it is available somewhere else other than registry, ask us if you could get that detail using C++. I guess this would be a better approach to solve your issue.
Added the information below since OP is looking for file version
See if the below code could help you.
CString GetFileVersionInfo(CString strFile, CString strProperty)
{
int rc;
UINT nLen;
DWORD nSize;
DWORD dwHandle = 0;
CString strBuffer;
CString strValue;
CString strBlock;
void *lpPropertyBuffer;
struct LANGANDCODEPAGE
{
WORD wLanguage;
WORD wCodePage;
} *lpTranslate;
nSize = GetFileVersionInfoSize(strFile.GetBuffer(strFile.GetLength()), &dwHandle);
::GetFileVersionInfo(strFile.GetBuffer(strFile.GetLength()), 0, nSize, strBuffer.GetBuffer(nSize));
// Read the list of languages and code pages.
if (VerQueryValue(strBuffer.GetBuffer(strBuffer.GetLength()), "\\VarFileInfo\\Translation", (LPVOID *) &lpTranslate, &nLen))
{
strBlock.Format("\\StringFileInfo\\%04x%04x\\%s",
lpTranslate->wLanguage,
lpTranslate->wCodePage,
strProperty);
rc = VerQueryValue(strBuffer.GetBuffer(strBuffer.GetLength()), strBlock.GetBuffer(nSize), &lpPropertyBuffer, &nLen);
if (rc != 0 && nLen > 0)
{
strncpy(strValue.GetBuffer(nLen + 1), (char *) lpPropertyBuffer, nLen);
strValue.ReleaseBuffer(nLen);
}
}
return strValue;
}
user version.lib while linking and you might need winver.h for compilation. You can call the function like this
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;
// initialize MFC and print and error on failure
if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
{
// TODO: change error code to suit your needs
cerr << _T("Fatal Error: MFC initialization failed") << endl;
nRetCode = 1;
}
else
{
AfxMessageBox(GetFileVersionInfo("shell32.dll", "ProductVersion"));
}
return nRetCode;
}
I'd say look at the file version information. And you might find this article useful on how the Add/Remove Programs dialog gets its information.
If the software developers chose not to add version information into Uninstall information, then there's no reliable way to get it.
You can try to find where application is installed. But even if you have the path, the application can consist of several .exe files which can have different versions and product names. If you add DLLs into the candidate list for getting version information, your results become even less predictable.