views:

992

answers:

5

What?

I have a DLGTEMPLATE loaded from a resource DLL, how can I change the strings assigned to the controls at runtime programmatically?

I want to be able to do this before the dialog is created, such that I can tell that the strings on display came from the resource DLL, and not from calls to SetWindowText when the dialog is initialized.

Google has found examples of creating DLGTEMPLATE in code, or twiddling simple style bits but nothing on editing the strings in memory.

How?

I am doing this by hooking the Dialog/Property Sheet creation API's. Which gives me access to the DLGTEMPLATE before the actual dialog is created and before it has a HWND.

Why?

I want to be able to do runtime localization, and localization testing. I already have this implemented for loading string (including the MFC 7.0 wrapper), menus and accelerator tables, but I am struggling to handle dialog/property sheet creation.

Code examples would be the perfect answer, ideally a class to wrap around the DLGTEMPLATE, if I work out my own solution I will post it.

+3  A: 

You can't edit the strings in memory. The DLGTEMPLATE structure is a direct file mapping of the relevent bytes of the resource dll. Thats read only.

You are going to need to process the entire DLGTEMPLATE structure and write out a new one with the altered length strings.

It will frankly be easier to just hook the WM_INITDIALOG and alter the strings by interacting with the controls than building a DLGTEMPLATE writer. Because there arn't a lot of those around. Unless you have an additional requirement to actually save altered dialog resources to disk as raw .res files (or attempt to modify the .dll inplace) Id really recommend you avoid this approach.

You say you are already doing this for accellerator tables and menu strings - if you can guarantee that the patched in strings are going to be shorter, then just make a binary copy of the DLGTEMPLATE struct, and write the non trivial scanning code necessary to find each string so you can patch the copy in place.

Chris Becke
A: 

See the API function ::EnumChildWindows( HWND, WNDENUMPROC, LPARAM )

You can call this in a CFormView::Create or CDialog::OnInitDialog to give yourself a chance to replace the control captions. Don't worry, the old strings don't flicker up before you replace them.

In your dialog resource, set the control captions to a key in some kind of dictionary. If you're compiling /clr you can use a managed string table resource. In your callback, look up the translated string in your dictionary and set the control's caption to the translation. Another benefit of /clr and managed string table is that you can automatically look up the right language by Windows (or you) having already set System::Threading::Thread::CurrentThread->CurrentUICulture.

Something like this

CMyDialog::OnInitDialog()
{
    ::EnumChildWindows(
        this->GetSafeHwnd(),
        CMyDialog::UpdateControlText,
        (LPARAM)this )
}

BOOL CALLBACK CMyDialog::UpdateControlText( HWND hWnd, LPARAM lParam )
{
    CMyDialog* pDialog = (CMyDialog*)lParam;
    CWnd* pChildWnd = CWnd::FromHandle( hWnd );

    int ctrlId = pChildWnd->GetDlgCtrlID();
    if (ctrlId)
    {
        CString curWindowText;
        pChildWnd->GetWindowText( curWindowText );
        if (!curWindowText.IsEmpty())
        {
            CString newWindowText = // some look up
            pChildWnd->SetWindowText( newWindowText );
        }
    }
}
Aidan Ryan
Thanks note quiet what I needed, but thanks for the reply
titanae
+1  A: 

You'll have to locate the string you want to modify in the mem buffer that represents the template. The only way to do that is to traverse the whole template. Which is not easy. Once you've done that, either insert bytes in the buffer if your new string is longer than the original one. Or shrink the buffer if the new string is shorter.

As Chris wrote, it would be much easier to modify the text in WM_INITDIALOG and try to re-phrase you requirement that says you may not call SetWindowText().

Serge - appTranslator
+2  A: 

There is a file out there somewhere (which I think originated at Microsoft but I am not completely sure) called RESFMT.ZIP which explains this with some code examples. Raymond Chen also does some excellent explanations of this on his blog. Note that the format of DIALOGEX and DIALOG controls are different.

As noted in some other answers you would need to create the structure again from the start. This isn't all bad as you already have the basic information. Adding the controls is where is gets hard.

Basically, allocate a largish block of memory into a WORD *lpIn. Then add the structure up on top of that. adding the basic information for the DIALOG (see DLGTEMPLATE) and the controls is pretty obvious as the information is there in MSDN.

The two biggest problems you will encounter are: Making sure that the various part start on an alignment boundary, and interpreting the values of DIALOG controls, especially when to add a just a string or, a string or ordinal. Each control needs to start on an even boundary.

For the first (borrowed from somewhere I think RESFMT.ZIP):

WORD *AlignDwordPtr (WORD *lpIn) { ULONG ul; ul = (ULONG) lpIn; ul +=3; ul >>=2; ul

What I did was build a series of functions like this one following that allowed me to assemble DIALOGS in memory. (My need was so I could have some common code that didn't need an associated RC file for some very basic messages).

Here is an example...

WORD *AddStringOrOrdinalToWordMem( WORD *lpw, char    *sz_Or_Ord )
    {
    LPWSTR  lpwsz;
    int     BufferSize;

    if (sz_Or_Ord == NULL)
        {
        *lpw++ = 0;
        }
    else
        {
        if (HIWORD(sz_Or_Ord) == 0) //MAKEINTRESOURCE macro 
            {
            *lpw++ = 0xFFFF;
            *lpw++ = LOWORD(sz_Or_Ord);
            }
        else
            {
            if (strlen(sz_Or_Ord))
                {
                lpwsz = ( LPWSTR ) lpw;
                BufferSize = MultiByteToWideChar( CP_ACP, 0, sz_Or_Ord, -1, lpwsz, 0 );
                MultiByteToWideChar( CP_ACP, 0, sz_Or_Ord, -1, lpwsz, BufferSize );
                lpw = lpw +  BufferSize;
                }
            else
                {
                *lpw++ = 0;
                }
            }
        }
    return( lpw );
    }

The header file to the complete module included these functions:

WORD *AddControlToDialogTemplateEx(MTDialogTemplateType *dlgtmp,
                                   char    *Title,
                                   WORD    Id,
                                   char    *WinClass,
                                   DWORD   Style,
                                   short   x,
                                   short   y,
                                   short   cx,
                                   short   cy,
                                   DWORD   ExStyle,
                                   int      HelpID);

int DestroyDlgTemplateEx(MTDialogTemplateType *dlgtmp);

MTDialogTemplateType *CreateDlgTemplateEx(  char    *Name, // We use     name just for reference, so it can be NULL
                                            short   x,
                                            short   y,
                                            short   cx,
                                            short   cy,
                                            DWORD   ExtendedStyle,
                                            DWORD   Style,
                                            char    *Menu,
                                            char    *WinClass,
                                            char    *Caption,
                                            char    *FontTypeFace,
                                            int     FontSize,
                                            int     FontWeigth,
                                            int     FontItalic,
                                            int     Charset,
                                            int     HelpID,
                                            int     NumberOfControls);

Which allowed me to assemble whole dialogs easily from code.

David L Morris
A: 

Thanks all, I actually had 24 hours rest on the problem, then went with a global windows hook filtering WM_INITDIALOG which was a much simpler method, worked out just fine, no API hooking required, 2 pages of code down to just a few lines.

Thanks for all the answers.

titanae
Mind posting your code?
Aidan Ryan
I'm wondering why a global hook was needed rather than just doing the replacement in InitDialog.
Aidan Ryan
The source bases is just too large, many UI libraries are used in multiple applications, a global hook was much simpler. The code shared by others is pretty much it, the global hook just catches WM_INITDIALOG before the target DlgProc
titanae