十进制数字的区域设置感知编辑控件子类化(格式[sign][xxx..][decimal separator][yy.])

Locale aware edit control subclassing for decimal numbers ( format [sign] [xxx...] [decimal separator] [yy...] )

本文关键字:xxx sign decimal yy separator 格式 设置 区域 感知 编辑 子类      更新时间:2023-10-16

简介和相关信息:

我有一个edit control,它应该只接受有符号的十进制数,类似于-123.456。此外,它应该具有区域设置意识,因为十进制分隔符并不是每个国家都相同。美国使用句点,而欧洲使用逗号等等

我为解决这个问题所做的努力:

到目前为止,我已经使用subclassing来实现这一点。以下是我实现subclassing的逻辑,通过伪代码表示:

if ( ( character is not a [ digit,separator, or CTRL/Shift... ] OR
( char is separator and we already have one ) )
{
discard the character;
}

首先,我制作了一个助手函数,用于确定char数组是否已经有十进制分隔符,如下所示:

bool HasDecimalSeparator( wchar_t *test )
{
// get the decimal separator
wchar_t szBuffer[5];
GetLocaleInfo ( LOCALE_USER_DEFAULT, 
LOCALE_SDECIMAL, 
szBuffer, 
sizeof(szBuffer) / sizeof(szBuffer[0] ) );
bool p = false; // text already has decimal separator?
size_t i = 0;   // needed for while loop-iterator
// go through entire array and calculate the value of the p
while( !( p = ( test[i] == szBuffer[0] ) ) && ( i++ < wcslen(test) ) );
return p;
}

这是subclass的过程-我没有考虑减号:

LRESULT CALLBACK Decimalni( HWND hwnd, UINT message, 
WPARAM wParam, LPARAM lParam, 
UINT_PTR uIdSubclass, 
DWORD_PTR dwRefData )
{
switch (message)
{
case WM_CHAR:
{
// get decimal separator
wchar_t szBuffer[5];
GetLocaleInfo ( LOCALE_USER_DEFAULT, 
LOCALE_SDECIMAL, 
szBuffer, 
sizeof(szBuffer) / sizeof(szBuffer[0] ) );
wchar_t t[50];  // here we store edit control's current text
memset( &t, L'', sizeof(t) );
// get edit control's current text
GetWindowText( hwnd, t, 50 );
// if ( ( is Not a ( digit,separator, or CTRL/Shift... )
// || ( char is separator and we already have one ) )
// discard the character
if( ( !( isdigit(wParam) || ( wParam == szBuffer[0] ) ) 
&& ( wParam >= L' ' ) )     // digit/separator/... ?
|| ( HasDecimalSeparator(t)        // has separator?    
&& ( wParam == szBuffer[0] ) ) )
{
return 0;
}
}
break;
}
return DefSubclassProc( hwnd, message, wParam, lParam);
}

一个重要的注意事项:由于这个问题的答案,我能够在我的应用程序中加载当前的用户区域设置。

问题:

有没有更好的方法来实现一个只接受带符号的十进制数字并且支持区域设置的编辑控件?

如果subclassing是唯一的方法,我的代码是否可以进一步改进/优化?

感谢您的时间和帮助。

致以最良好的问候。

附录:

为了进一步帮助您,这里有一个小的演示应用程序,它创建了一个编辑控件,subclass允许它只接受十进制数字

#include <windows.h>
#include <commctrl.h>
#include <stdlib.h>
#include <locale.h>
#pragma comment( lib, "comctl32.lib")
const wchar_t g_szClassName[] = L"myWindowClass";
bool HasDecimalSeparator( wchar_t *test )
{
// get the decimal separator
wchar_t szBuffer[5];
GetLocaleInfo ( LOCALE_USER_DEFAULT, 
LOCALE_SDECIMAL, 
szBuffer, 
sizeof(szBuffer) / sizeof(szBuffer[0] ) );
bool p = false; // text already has decimal separator?
size_t i = 0;   // needed for while loop-iterator
// go through entire array and calculate the value of the p
while( !( p = ( test[i] == szBuffer[0] ) ) && ( i++ < wcslen(test) ) );
return p;
}
LRESULT CALLBACK Decimalni( HWND hwnd, UINT message, 
WPARAM wParam, LPARAM lParam, 
UINT_PTR uIdSubclass, 
DWORD_PTR dwRefData )
{
switch (message)
{
case WM_CHAR:
{
// get decimal separator
wchar_t szBuffer[5];
GetLocaleInfo ( LOCALE_USER_DEFAULT, 
LOCALE_SDECIMAL, 
szBuffer, 
sizeof(szBuffer) / sizeof(szBuffer[0] ) );
wchar_t t[50];  // here we store edit control's current text
memset( &t, L'', sizeof(t) );
// get edit control's current text
GetWindowText( hwnd, t, 50 );
// if ( ( is Not a ( digit,separator, or CTRL/Shift... )
// || ( char is separator and we already have one ) )
// discard the character
if( ( !( isdigit(wParam) || ( wParam == szBuffer[0] ) ) 
&& ( wParam >= L' ' ) )     // digit/separator/... ?
|| ( HasDecimalSeparator(t)        // has separator?    
&& ( wParam == szBuffer[0] ) ) )
{
return 0;
}
}
break;
}
return DefSubclassProc( hwnd, message, wParam, lParam);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_CREATE:
{
/************* load current locale settings *************/
// max. len: language, country, code page
wchar_t lpszLocale[64+64+16+3] = L""; 
wchar_t lpszVal[128];
LCID nLCID = ::GetUserDefaultLCID(); // current LCID for user
if ( ::GetLocaleInfo( nLCID, LOCALE_SENGLANGUAGE, lpszVal, 128 ) )
{
wcscat_s( lpszLocale, 147, lpszVal ); // language
if ( ::GetLocaleInfo( nLCID, LOCALE_SENGCOUNTRY, lpszVal, 128 ) )
{
wcscat_s( lpszLocale, 147, L"_" ); // append country/region
wcscat_s( lpszLocale, 147, lpszVal );
if ( ::GetLocaleInfo( nLCID, 
LOCALE_IDEFAULTANSICODEPAGE, lpszVal, 128 ) )
{ 
// missing code page or page number 0 is no error 
// (e.g. with Unicode)
int nCPNum = _wtoi(lpszVal);
if (nCPNum >= 10)
{
wcscat_s( lpszLocale, 147, L"." ); // append code page
wcscat_s( lpszLocale, 147, lpszVal );
}
}
}
}
// set locale and LCID
_wsetlocale( LC_ALL, lpszLocale );
::SetThreadLocale(nLCID);
/*************************************************/
HWND hEdit1;
hEdit1 = CreateWindowEx(0, L"EDIT", L"", 
WS_BORDER | WS_CHILD | WS_VISIBLE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 
50, 100, 100, 20, 
hwnd, (HMENU)8001, GetModuleHandle(NULL), NULL);
SetWindowSubclass( hEdit1, Decimalni, 0, 0);
}
break;
case WM_SETTINGCHANGE:
if( !wParam && !wcscmp( (wchar_t*)lParam, L"intl" ) )
{
// max. len: language, country, code page
wchar_t lpszLocale[64+64+16+3] = L""; 
wchar_t lpszVal[128];
LCID nLCID = ::GetUserDefaultLCID(); // current LCID for user
if ( ::GetLocaleInfo( nLCID, LOCALE_SENGLANGUAGE, lpszVal, 128 ) )
{
wcscat_s( lpszLocale, 147, lpszVal ); // language
if ( ::GetLocaleInfo( nLCID, LOCALE_SENGCOUNTRY, lpszVal, 128 ) )
{
wcscat_s( lpszLocale, 147, L"_" ); // append country/region
wcscat_s( lpszLocale, 147, lpszVal );
if ( ::GetLocaleInfo( nLCID, 
LOCALE_IDEFAULTANSICODEPAGE, lpszVal, 128 ) )
{ 
// missing code page or page number 0 is no error
// (e.g. with Unicode)
int nCPNum = _wtoi(lpszVal);
if (nCPNum >= 10)
{
wcscat_s( lpszLocale, 147, L"." ); // append code page
wcscat_s( lpszLocale, 147, lpszVal );
}
}
}
}
// set locale and LCID
_wsetlocale( LC_ALL, lpszLocale );
::SetThreadLocale(nLCID);
return 0L;
}
else
break;
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, msg, wParam, lParam);
}
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASSEX wc;
HWND hwnd;
MSG Msg;
wc.cbSize        = sizeof(WNDCLASSEX);
wc.style         = 0;
wc.lpfnWndProc   = WndProc;
wc.cbClsExtra    = 0;
wc.cbWndExtra    = 0;
wc.hInstance     = hInstance;
wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.lpszMenuName  = NULL;
wc.lpszClassName = g_szClassName;
wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);
if(!RegisterClassEx(&wc))
{
MessageBox(NULL, L"Window Registration Failed!", L"Error!",
MB_ICONEXCLAMATION | MB_OK);
return 0;
}
hwnd = CreateWindowEx(
0,
g_szClassName,
L"theForger's Tutorial Application",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 480, 320,
NULL, NULL, hInstance, NULL);
if(hwnd == NULL)
{
MessageBox(NULL, L"Window Creation Failed!", L"Error!",
MB_ICONEXCLAMATION | MB_OK);
return 0;
}
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
return Msg.wParam;
}

考虑区域设置

您当然可以自己做任何事情,但是您可以选择使用VarI4FromStr或类似的API,它为您做一些肮脏的事情。你把字符串放进去,你就得到了LONG。区域设置意识。

"应仅接受">

您没有指定控件应该如何准确地执行此操作。如果输入字符串无效怎么办?控件应该仍然接受它,因为,例如,字符串仍然无效,并且用户仍在键入。如果您在外部处理程序中验证输入,例如按下OK按钮时,那么您甚至不需要子类化。如果您想在每次输入更改时都检查它,那么也不需要子类化,因为父级上有EN_CHANGE通知。不过,您可能出于其他原因想要进行子类化。

用户友好的做法是接受任何输入,然后在文本更改或输入验证时以某种方式指示有效性(例如,如果无效,用红色下划线)。

在考虑了从剪贴板获取文本后将一个字符串插入另一个字符串所需的Advice中的代码后,我能够提出满足要求的子类化过程。

我的解决方案的重点是模拟编辑控件的行为,如文章中所述,然后验证生成的文本。

在处理VK_DELETE时,所选文本将被删除,然后对结果进行分析,以检查是否保留了有效的十进制格式。如果一切正常,则消息被传递到默认过程,否则将被丢弃。在WM_CHAR处理程序中,对WM_CUTWM_CLEARbackspace执行了相同的方法(在这里,我们必须通过访问序号为-1的字符串元素来保护自己免受应用程序崩溃的影响,这就是我添加行if ( start > 0 )的原因)。

在处理WM_PASTE时,我们将编辑控件的文本与剪贴板文本合并,然后解析生成的字符串以检查其有效性。同样,如果一切正常,我们会传递消息,否则我们会丢弃它

同样的事情也适用于WM_CHAR,只是我们在这里将字符插入编辑控件文本的选定部分,然后执行有效性检查。

由于这样输入的文本总是正确的,所以我们不必处理WM_UNDO

最后,这里是代码:

LRESULT CALLBACK Decimalni( HWND hwnd, UINT message, 
WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
{
switch (message)
{
case WM_KEYDOWN:
{
if( wParam == VK_DELETE )
{
DWORD start, end;
int len = GetWindowTextLength(hwnd);
std::wstring buffer( len, 0 );
// get current window text
if( len > 0 )
GetWindowText( hwnd, &buffer[0], len + 1 );
// get current selection
SendMessage( hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end );
if( end > start )
buffer.erase( start, end - start );
else
buffer.erase( start, 1 );
if( buffer.empty() )
return ::DefSubclassProc( hwnd, message, wParam, lParam);
bool IsTextValid = true; // indicates validity of inputed text
// TODO: parse buffer
if( IsTextValid )
return ::DefSubclassProc( hwnd, message, wParam, lParam);
else
{
// TODO: indicate error
return FALSE;
}
}
}
return ::DefSubclassProc( hwnd, message, wParam, lParam);;
break;
case WM_CLEAR:
case WM_CUT:
{
DWORD start, end;
int len = GetWindowTextLength(hwnd);
std::wstring buffer( len, 0 );
// get current window text
if( len > 0 )
GetWindowText( hwnd, &buffer[0], len + 1 );
// get current selection
SendMessage( hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end );
if( end > start )
buffer.erase( start, end - start );
if( buffer.empty() )
return ::DefSubclassProc( hwnd, message, wParam, lParam);
// TODO: parse buffer 
bool IsTextValid = true;
if( IsTextValid )
return ::DefSubclassProc( hwnd, message, wParam, lParam);
else
{
// TODO: Indicate error
return FALSE;
}
}
break;
case WM_PASTE:
{
int len = GetWindowTextLength(hwnd);
std::wstring clipboard, wndtxt( len, 0 );
if( len > 0 )
GetWindowText( hwnd, &wndtxt[0], len + 1 );
if( !OpenClipboard(hwnd) )
return FALSE;
HANDLE hClipboardData;
if( hClipboardData = GetClipboardData(CF_UNICODETEXT) )
{
clipboard = (wchar_t*)GlobalLock(hClipboardData);
GlobalUnlock(hClipboardData);  
}
CloseClipboard();
if( clipboard.empty() )
return FALSE;
DWORD start, end;
SendMessage( hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end );
// merge strings into one
if( end > start )
wndtxt.replace( start, end - start, clipboard );
else
wndtxt.insert( start, clipboard );
// TODO: parse the text
bool ITextValid = true;
// process the result
if( IsTextValid )
return ::DefSubclassProc( hwnd, message, wParam, lParam);
else
{
// TODO: indicate error
return FALSE;
}
}
break;
case WM_CHAR:
{
DWORD start, end;
int len = GetWindowTextLength(hwnd);
std::wstring buffer( len, 0 );
// get current window text
if( len > 0 )
GetWindowText( hwnd, &buffer[0], len + 1 );
// get current selection
SendMessage( hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end );
// allow copy/paste but leave backspace for special handler
if( ( wParam < 0x020 ) && ( wParam != 0x08 ) )
return ::DefSubclassProc( hwnd, message, wParam, lParam);}
// process backspace
if( wParam == 0x08 ) 
{
if( end > start )
buffer.erase( start, end - start );
else
if( start > 0 )    // it is safe to move back one place
buffer.erase( start - 1, 1 );
else  // start-1 < 0 , can't access buffer[-1] !!
return FALSE;
if( buffer.empty() )
return ::DefSubclassProc( hwnd, message, wParam, lParam);
// TODO: parse buffer
// process the result
if( IsTextValid )
return ::DefSubclassProc( hwnd, message, wParam, lParam);
else
{
//TODO: indicate error
return FALSE;
}
}
// insert character and parse text
if( end > start )
buffer.replace( start, end - start, 1, (wchar_t)wParam );
else
buffer.insert( start, 1, (wchar_t)wParam );
// TODO: parse text
// process the result
if( IsTextValid )
return ::DefSubclassProc( hwnd, message, wParam, lParam);
else
{
//TODO: indicate error
return FALSE;
}
}
break;
case WM_NCDESTROY:
::RemoveWindowSubclass( hwnd, Decimalni, 0 );
break;
}
return ::DefSubclassProc( hwnd, message, wParam, lParam);
}