How to disable Edit control's focus on Dialog first launch? - c++

Hello everybody reading this. Thanks in advance for your time.
One thing before question: I DO NOT use neither MFC nor Windows Forms, just WinApi in C++.
Well, I am making a polynomial calculator in Visual C++. I added a Dialog to it, which was created in resources (.rc file) using drag'n'drop method. I suppose there would be no such a problem if i created my Dialog with CreateWindowEx (but I don't want to).
My Dialog has a few of Edit Controls. Everything is fine except that when the Dialog is launched, one of Edit controls takes focus to be ready to take keyboard input.
I have included management of EN_KILLFOCUS (Edit sends it to parent when loses focus due to selecting another control).
Here I read from control to wstring (string of wide characters - _UNICODE is defined), use some kind of parser to verify this wstring and remove bad characters, and then put correct string into the same edit control. It works fine, but here is the source of my problem:
When there was no input, parser returns string "0" (not the NULL, string is just set to "0"), as if control had focus and then lost it even before I clicked anything in Dialog.
Due to that, and something else (this is what I have to figure out), at the Dialog launch parser puts this string "0" to edit.
I want to make my edit not be able to take input from keyboard until i click one of the Edits (including this one).
If it is not possible, I want to clear the whole text at the beginning of dialog (being able to take input is not a problem, I just want to prevent parser from entering string "0" at the beginning)
My code:
In DlgProc I have:
//up here is switch to manage all controls
case MyEditID: // here is ID of one of my edits from resources
switch (HIWORD(wParam))
{
case EN_KILLFOCUS: // edit lost focus - another control selected
if (LOWORD(wParam)==MyEditID) //necessary to determine if
// one of allowed Edits sent this message
// because I have also other Edits
{
GetDlgItemText(hPanel, LOWORD(wParam), MyTempWcharArray, 100);
MyTempString.assign(MyTempWcharArray);
w1 = polynomial(MyTempWcharArray); // parser takes the string
// and removes bad chars in constructor
// polynomial is my class - you don't have to care of it
// w1 is declared before as object of polynomial class
MyTempString = w1.ConversionToString();
SetDlgItemText(hDialog, LOWORD(wParam), sw1);
}
break;
}
break;
does it matter what integer number is set to Edit's ID?
I know SetFocus(), and WM_SETFOCUS message. In this case I just can't get this working.
If i haven't included something important to make you see my point please let me know. I'm sorry I'm just a newbie in WinAPI world.
EDIT:
For those with a similar problem: Do not do this:
I made an workaround with global variable ProcessKillFocus set to false indicating that instructions in message management should not be processed, except that at the end (just before break;) I am changing it to true, so next time and later it will be processed:
case EN_KILLFOCUS:
if (ProcessKillFocus && LOWORD(wParam)==MyEditID)
{
// first time global ProcessKillFocus is false so all this is skipped
// 2nd time and later do all the stuff
}
ProcessKillFocus = true;
break;
Huge thanx to Sheyros Adikari for making my question easy to understand!!!
Huge thanx to patriiice for simple answer on a huge messing question!!!
ANSWER:
BTW: patriiice, I tried this:
case WM_INITDIALOG:
SetFocus(GetDlgItem(hDialog, Desired_Control_ID));
return (INT_PTR)FALSE;
break;
IT JUST WORKS!!!

You have to return FALSE to WM_INITDIALOG message and set the correct focus by yourself.

Related

C++ MFC - CEdit / EDITTEXT Control - only allow certain chars

Thank you for the answers and comments. I chose the answer I chose because it allowed me to continue to use CEdit with just a couple of minor changes to the code. However, the solution considering CMFCMaskedEdit also seemed to work as well when tested. If you choose to use that solution make sure you apply the correct functions for the object such as SetValidChars etc upon initialisation ! :) Thank you again everyone
I am using Visual Studio Professional 2017 C++ with MFC
I have a CEdit object in my MFC project which also has an EDITTEXT control in my .rc file.
The CEdit object will be edited by the user who will type a keyword, and I will do something with that keyword, that is, find files that contain that keyword.
Naturally, due to my task, I cannot allow the following char s: \ / : * ? " < > | , since these chars are not allowed to be in a file or folder name.
What can I do to prevent a user from entering these characters into the CEditBox. Realistically, the only chars I will need are: A-Z, a-z, 0-9, and _.
Another specification: no regex please ! Ideally the answer will use a Control (I looked here) or function (I looked here) I may have overlooked.
If there is no solution, I will fall back to this:
I will check whether any of these chars are in the text the user entered. If no, awesome, nothing to worry about ! If yes, then I will return an error :)
Thank you in advance ! :D
I can think of two possible solutions to your question. The 1st solution posted just below is the easiest to implement because it does not require subclassing the control.
1st Solution - Control Notification
Edit controls send the EN_UPDATE notification, just before the (updated) text is about to be displayed. You can capture this event easily: open the Resource Editor, go to the dialog, select the edit conrol and in the Properties Editor go to Control Events page and Add the EN_UPDATE handler. The editor will add the handler to the message-map and generate the function:
BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
.
.
ON_EN_UPDATE(IDC_EDIT_FNAME, &(CMyDialog::OnEnUpdateEditFname)
END_MESSAGE_MAP()
In the generated function add the following code:
void CMyDialog::OnEnUpdateEditFname()
{
CString s;
GetDlgItemText(IDC_EDIT_FNAME, s); // Get the control's text - may contain illegal characters
// First illegal character position
int nFIChar = -1;
// Loop until all illegal chars are removed - will also work for a paste operation w/ multiple illegal chars
while (LPCTSTR p = _tcspbrk(s, _T("\\/:*?\"<>|")))
{
if (nFIChar<0) nFIChar = p-s; // Store 1st illegal char position
s.Remove(*p); // Remove illegal char(s)
}
if (nFIChar>=0) // At least one illegal char found
{ // Replace the control's text and display a balloon
CEdit *pEdit = (CEdit*)GetDlgItem(IDC_EDIT_FNAME);
pEdit->SetWindowText(s); // SetWindowText() will reset the caret position!
pEdit->SetSel(nFIChar, nFIChar); // Set caret to the 1st illegal character removed
MessageBeep(-1);
pEdit->ShowBalloonTip(NULL, _T("A file name can't contain any of the following characters:\n\t\\ / : * ? \" < > | "));
}
}
This will remove the illegal characters and will display a balloon tip, like when entering an illegal character while trying to rename a file in File Explorer. It's tested and works.
Alternative Solution - Subclassing
Another solution is possible, employing a subclassed control class:
Define a CEdit-derived class.
Add a handler for the WM_CHAR message.
In the WM_CHAR handler, if an illegal character is about to be entered, beep and display the balloon, but do NOT call the default, otherwise call it.
So the code could be:
BEGIN_MESSAGE_MAP(CFilenameEdit, CEdit)
ON_WM_CHAR()
END_MESSAGE_MAP()
void CFilenameEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if (_tcschr(_T("\\/:*?\"<>|"), nChar))
{
MessageBeep(-1);
ShowBalloonTip(NULL, _T("A file name can't contain any of the following characters:\n\t\\ / : * ? \" < > | "));
}
else CEdit::OnChar(nChar, nRepCnt, nFlags);
}
You may want to add a handler for the WM_PASTE message too.
Then you have to use it in your dialog, just use the Class Wizard to add a member variable of the derived edit class, associated with the edit control. It can be easily reused in another project.
EDIT:
The 1st solution (capturing the EN_UPDATE notification) is easier to implement (although there's more code in this sample - the 2nd one doesn't currently handle the paste operations) because it does not require defining a new subclass. It's what a developer would choose to handle a special requirement, quickly implementing it for the project.
The 2nd solution defines a new subclass. It can be reused in another project - I tend to favor reusable code - but it needs to be completed (handle paste operations as well) and then maintained. And in order to be more useful it should preferably be enhanced, for example make it more general, like add an option for fully-qualified path/file names (they may contain \, : or ") or better yet allow the developer to define the set of invalid characters - in this case the message displayed should also be defined by the developer*, as the new class could be used in more cases, not just for filenames or paths. So this would require more work initially, and it's finally a matter of choice (a bigger "upfront investment", with potential future benefits).
* The 2nd line of the message, containing the invalid character list should be constructed programmatically, by the class's code
Note: The _tcspbrk() and _tcschr() (THCAR.H versions of strpbrk() and strchr()) are CRT functions. One could alternatively use the StrPBrk() or StrCSpn() and StrChr() functions from Shlwapi - many useful utility functions there btw.
I suggest you switch to using the CMFCMaskedEdit class instead of CEdit. It supports exactly the behavior you are after.

How to eliminate the MessageBeep from the RICHEDIT control?

The RichEdit control has this very annoying feature. It beeps every time the user tries to move the cursor past its "end point". For instance, you can test it with the WordPad that also implements RICHEDIT. Open it up, type in some text, then hit the Home key. If the cursor was not in the beginning of the line:
hitting Home key will move it there, but then hitting the Home key again will produce this beep.
At first glance it seemed like overriding WM_KEYDOWN and WM_KEYUP messages and blocking the situations when RICHEDIT can produce that beep was a solution ... until I actually started implementing it. Unfortunately though, it's not as simple as it sounds, as that control beeps in a LOT OF CASES! So my keystroke blocking code literally ballooned to over 300+ lines, and I still see that there are some key-presses that I either didn't account for, or worse, I might have overridden some useful behavior with. (Read below for more details.)
Then I decided to look inside the implementation of the RICHEDIT control itself. And sure enough, for instance if we look at the implementation of the Home key press, the C:\WINDOWS\SysWOW64\msftedit.dll on my Windows 10 OS, has the function called ?Home#CTxtSelection##QAEHHH#Z (or public: int __thiscall CTxtSelection::Home(int,int) demangled) at the mapped offset 0x3FC00, that is hard-coded to call the MessageBeep(MB_OK), or exactly what I'm trying to eliminate:
And if you look at the address 0x6B64FD38 in the screenshot above, there's a built-in way to bypass it, with what looks to be flag 0x800.
So having dug into msftedit.dll a little bit more, there appears to be a function called ?OnAllowBeep#CTxtEdit##QAEJH#Z (or public: long __thiscall CTxtEdit::OnAllowBeep(int) demangled) that can modify this flags:
After a bit more research I found out that there are COM interfaces built into RICHEDIT control, such as ITextServices and ITextHost that reference that flag as TXTBIT_ALLOWBEEP in ITextServices::OnTxPropertyBitsChange method.
Unfortunately though, I can't seem to find the way how I can directly change that TXTBIT_ALLOWBEEP flag (COM is not my forte.) I tried looking into implementing ITextHost, but it has a lot of virtual methods that have nothing to do with what I'm trying to achieve that I don't know how to implement.
Does anyone have any idea how to clear that TXTBIT_ALLOWBEEP flag?
PS. Here's why I didn't go the route of overriding key-presses:
Just to give you an example. Say, if I override the VK_HOME key press. I need to make sure that the cursor is not at the beginning of the line, but also that there's no selection. Yet, I need to make sure that Ctrl key is not down in a situation when the cursor is at the very top of the window. Then the same with the Shift key, and I'm not even sure what Alt does with it ... and so forth. Oh, and this is just the Home key. There's also Up, Down, Left, Right, PageUp, PageDown, End, Delete, Backspace. (And that's what I was aware of. There may be more, plus I'm not even talking about IME or other keyboard layouts, etc.) In other words, it becomes a mess!
So, eventually I realized that anticipating a keystroke is not the way to go.
first we need send EM_GETOLEINTERFACE message to rich edit window - this is Retrieves an IRichEditOle object that a client can use to access a rich edit control's Component Object Model (COM) functionality.
then for retrieve an ITextServices pointer, call QueryInterface on the private IUnknown pointer returned by EM_GETOLEINTERFACE.
here exist interesting point - the IID_ITextServices not well known but need get in runtime from Msftedit.dll
from About Windowless Rich Edit Controls
Msftedit.dll exports an interface identifier (IID) called IID_ITextServices that you can use to query the IUnknown pointer for the ITextServices interface.
after we got ITextServices pointer - we simply can call OnTxPropertyBitsChange with TXTBIT_ALLOWBEEP mask
code example:
#include <textserv.h>
if (HMODULE hmodRichEdit = LoadLibrary(L"Msftedit.dll"))
{
// create richedit window
if (HWND hwndRich = CreateWindowExW(0, MSFTEDIT_CLASS, ...))
{
if (IID* pIID_ITS = (IID*) GetProcAddress(hmodRichEdit, "IID_ITextServices"))
{
IUnknown* pUnk;
if (SendMessageW(hwndRich, EM_GETOLEINTERFACE, 0, (LPARAM)&pUnk))
{
ITextServices* pTxtSrv;
HRESULT hr = pUnk->QueryInterface(*pIID_ITS, (void**)&pTxtSrv);
pUnk->Release();
if (0 <= hr)
{
pTxtSrv->OnTxPropertyBitsChange(TXTBIT_ALLOWBEEP, 0);
pTxtSrv->Release();
}
}
}
}
}

MFC - Remove leading zeros for CEdit number control

I find that CEdit control has option 'Number' in its property, so that I can prevent user from enter non-digit character into this textbox - it is CEdit number control now.
If there is an option 'Number', I think maybe there is a way to remove leading zeros for CEdit which is just simple like option 'Number'.
I have tried Dialog Data Exchange with hope that it would remove leading zeros for me automatically, but it won't.
Then I think the way to do this is add EN_KILLFOCUS message for each of the CEdit number controls, but I find that exhausted.
So I think the better way to do that is add EN_KILLFOCUS, but all the CEdit number controls lose focus event point to one function, in this function I'll remove leading zero for the 'current' control, but in C# I can get the 'current' control, in C++ I don't know if it's supported.
Or inherit CEdit to make CEditNum - which implement lose focus remove leading zeros feature, but with this solution, I can't design it on the Visual Studio design window (I think). I hope there is a solution similar to this solution (which is a solution for Draw&Drop problem)
Anyway, before apply the final solution (EN_KILLFOCUS), I want to make sure if is there better way - least implement, reuse the existing implement of MFC.
A little explain about remove leading zeros: you enter: 00001 into the CEdit control, then lose focus, the CEdit control show you: 1. The idea is like MS Excel when you enter a number into its cell.
"but all the CEdit number controls lose focus event point to one function"
That is true, but you get the control ID of the control that's just lost focus as a parameter.
Add this to your Message table, replace IDC_FIRST, IDC_LAST with the first and last IDs of your edit controls, or use 0, 0xFFFFFFFF for all.
ON_CONTROL_RANGE(EN_KILLFOCUS, IDC_FIRST, IDC_LAST, OnKillFocus).
Here is the signature of OnKillFocus, and how to get a CWnd to apply changes.
void CMyDialogClass::OnKillFocus(UINT nID)
{
// you can further check if the ID is one of interest here...
// if your edit control control IDs are not contiguous, for example.
// you can get a CEdit* here, but only if you used DDX to map the
// control to a CEdit.
CWnd* pCtrl = GetDlgItem(nID);
if (pCtrl)
{
CString str;
pCtrl->GetWindowText(str);
// remove zeroes, or format as you like....
str.Format(_T("%d"), _tcstoi(str));
pCtrl->SetWindowText(str);
}
}
// if you mapped the control to a CEdit, here's how you can safely
// get a pointer to a CEDit
CEdit* pEdit = (CEdit*)GetDlgItem(nID);
ASSERT_KINDOF(CEdit, pEdit); // debug check
if (pEdit && pEdit->IsKindOf(RUNTIME_CLASS(CEdit))) // standard check
{
// ....
}

Simulate keyboard input inside app in C++ MFC app

I created a dialog MainDialog.cpp with 2 edit controls whose IDs are IDC_EDITCONTROL_A and IDC_EDITCONTROL_B, and have variables defined as m_editControlA and m_editControlB, respectively.
Also, I have 2 buttons whose IDs are IDC_MFCBUTTON_KEY_X and IDC_MFCBUTTON_KEY_Y, and variables are m_buttonKeyX and m_buttonKeyY, respectively.
Below is the code in the source file
#include "afxdialogex.h"
IMPLEMENT_DYNAMIC(CMainDialog, CDialogEx)
CMainDialog::CMainDialog(CWnd* pParent): CDialogEx(IDD_MAIN_DIALOG, pParent)
{
}
CMainDialog::~CMainDialog()
{
}
void CMainDialog::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDITCONTROL_A, m_editControlA);
DDX_Control(pDX, IDC_EDITCONTROL_B, m_editControlB);
DDX(Control(pDX, IDC_MFCBUTTON_KEY_X, m_buttonKeyX);
DDX(Control(pDX, IDC_MFCBUTTON_KEY_Y, m_buttonKeyY);
}
BEGIN_MESSAGE_MAP(CMainDialog, CDialogEx)
ON_EN_CHANGE(IDC_EDITCONTROL, &CMainDialog::OnEnChangeEditA)
ON_BN_CLICKED(IDC_MFCBUTTON_KEY_X, &CMainDialog::OnBnClickedButtonX)
ON_BN_CLICKED(IDC_MFCBUTTON_KEY_Y, &CMainDialog::OnBnClickedButtonY)
END_MESSAGE_MAP()
void CMainDialog::OnBnClickedButtonX()
{
m_editControlA.SetWindowTextW(_T("X")); // test
}
void CMainDialog::OnBnClickedButtonX()
{
m_editControlA.SetWindowTextW(_T("Y")); // test
}
I am trying to understand how I can have each button send their respective character (i.e. X or Y in this example) to the selected edit control if one is selected. Essentially, I would like to simulate keyboard input.
I have read the docs about how to simulate keyboard events and also the sendMessage but I could not understand how to implement it since my C++ knowledge is very basic. Also, following my previous question I have found that the GetFocus would be useful but still my main issue currently is sending the input.
Any example code or useful link could be very useful for me to learn how I can simulate a keyboard input inside an app.
The characters are sent from the OS to the edit controls using the WM_CHAR message.
In reality it is a bit more complex than that, but you do not need to emulate the entire WM_KEYUP WM_KEYDOWN message sequence, since its end result is to generate a WM_CHAR message.
You can use CWnd::PostMessage to send characters directly to your edit controls, even when they do not have the focus.
You have probably already found the documentation for WM_CHAR here: https://msdn.microsoft.com/fr-fr/library/windows/desktop/ms646276(v=vs.85).aspx
oops.. excuse my french, ths english doc is here
https://msdn.microsoft.com/en-us/library/windows/desktop/ms646276(v=vs.85).aspx
(just changing the fr-fr to en-us does the trick, it probably works for all other languages, neat!
wParam holds the character you want to send. Either an plain ASCII character, or one of the VK_ constants... I suggest you use the unicode version WM_CHARW, as most windows software uses unicode nowadays. The notation for wide chars is either L'X' or _T('X'), the unicode (UTF-16) character type is wchar_t.
lParam contains other keystroke details, 0 should be fine for what you want to do.
to send X, simply call
m_editControlA.PostMessage(WM_CHAR, _T('X'));
When using the _T() notation, the character (or string) literal between the parenthesis will be automatically converted to the right character width for your app's unicode setting (you should set that to UNICODE, since that's what the OS is using, and is also the only valid encoding for Windows CE, for example, and you should get used to manipulating this type.
the _T() macros and _t* overrides for almost all C library functions operating on strings are defined in tchar.h, which is included by Visual Studio in stdafx.h. Under MFC, you'll mostly use CString, but it's good to know where these things are.
[EDIT] When you get that running, you should start playing with WM_KEYDOWN. You will discover that PostMessage(WM_CHAR, VK_ESCAPE) directly to your dialog does not close it, while a PostMessage(WM_KEYDOWN, VK_ESCAPE) does. And that m_editBox.PostMessage(WM_KEYDOWN, _T('X')) will send a lower key 'x' to your edit box. But that's another topic to ivestigate.
Have fun with MFC!
For your last question:
Sure, but it gets a bit more complicated, as your button will gain focus, as soon as you click on it. You'd have to create handlers for EN_SETFOCUS for eeach of your edit boxes, and add a CWnd* data member to keep track of the last edit box that had focus.
Your EN_SETFOCUS handlers should look something like this
void CdlgDlg::OnEnSetfocusEdit1()
{
m_pWndLastFocus = &m_edit1;
}
Don't forget to set the pointer to NULL in your constructor and to chjeck it's valid before calling m_pWndLastFocus->PostMessage() though.
The way to synthesize input in MFC is by using the SendInput method.

How to implement the mouse click for URLs at rich edit control

I added a read-only rich edit 2.0 control to my dialog (code is using C windows API, the dialog is created by using function DialogBox)
At the dialog call back, at the WM_INITDIALOG, I add the following code to enable url detection and also enable the event ENM_LINK is sent to the parent dialog instead of the rich edit control itself:
LRESULT mask = SendMessage(hWndText, EM_GETEVENTMASK, 0, 0); //hWndText is rich edit control
SendMessage(hWndText, EM_SETEVENTMASK, 0, mask | ENM_LINK);
::SendMessage(hWndText, EM_AUTOURLDETECT, TRUE, NULL);
I had a little trouble to enable the url detection when dialog is initially launched (which seems a known issue or behavior since rich edit control would only enable url detection of modified text). However I worked around this issue by setting the dialog text again on every WM_PAINT event.
The code is generally working. I also implemented the following code to launch the URL at the browser when mouse is hovering over the url:
case WM_NOTIFY:
plink = (ENLINK *) lParam;
switch(LOWORD(wParam))
{
case IDC_DISPLAY_TEXT_2: //this is ID for my rich edit control
szURL =m_strDisplay.Mid(plink->chrg.cpMin, plink->chrg.cpMax - plink->chrg.cpMin);
LaunchURL(szURL); //function to launch the url with default browser
break;
default:
break;
}
It seems that I would get WM_NOTIFY event every time when I hovered the mouse over the url. However when I clicked on it, I always get same event as the mouse hover over.
Based on the structure of ENLINK, I should get more detailed NM event at the NMHDR structure, however the value plink->nmhdr.code is always 1803 which is not even NM_HOVER (its defined value is (NM_FIRST-13) and NM_FIRST is (0U- 0U), so NM_HOVER value is 4294967283 on my 64 bit machine). I know that I am missing something here. Could someone shed some lights here? How can I get the mouse click event for the rich edit control?
I think you should capture the EN_LINK notification. I implemented the following code. It enables a url link in a richedit control placed into the parent window, not into a dialog. You could adapt it for your dialog, as well.
Consider beginning with the code:
case WM_NOTIFY: {
switch (((LPNMHDR)lParam)->code) { //NMHDR structure contains information about a notification message.
case EN_LINK: {
ENLINK *enLinkInfo = (ENLINK *)lParam; // pointer to a ENLINK structure
then, if you choose to launch url on LBUTTONUP, you have to check the value contained in enLinkInfo->msg (remember to adapt it for your dialog, though)
if (enLinkInfo->msg == WM_LBUTTONUP) {
// select all the text from enLinkInfo->chrg.cpMin to enLinkInfo->chrg.cpMax
// lauch the url
}
Besides, you can intercept WM_MOUSEMOVE:
if(enLinkInfo->msg == WM_MOUSEMOVE) {
; // do nothing
}
Hope it helps.
As the answer by #A_nto2 shows, to intercept a mouse click do:
case WM_NOTIFY: {
//NMHDR structure contains information about a notification message.
switch (((LPNMHDR)lParam)->code) {
case EN_LINK: {
ENLINK *enLinkInfo = (ENLINK *)lParam; // pointer to a ENLINK structure
if (enLinkInfo->msg == WM_LBUTTONUP) {
But the tricky part is to get the link that was clicked on.
One gets a "range" that was clicked on in the enLinkInfo->chrg of the type CHARRANGE.
An answer to Detect click on URL in RichEdit suggests using the EM_EXSETSEL with the enLinkInfo->chrg. And then using the EM_GETSELTEXT to retrieve the text.
That works with auto-detected plain-text URLs (EM_AUTOURLDETECT).
A problem is with friendly name hyperlinks (i.e. those that have an anchor text different than the URL itself):
{\rtf1{\field{\*\fldinst{ HYPERLINK "https://www.example.com"}}{\fldrslt{Example}}}}
(Note that these are supported in Rich Edit 4.1 and newer only)
For these, the CHARRANGE points to the HYPERLINK "https://www.example.com" part, which is hidden and cannot be selected using the EM_EXSETSEL. Actually it can be selected on Windows 10. But it cannot be selected on Windows 7, Vista and XP. Sending the EM_EXSETSEL to these systems results in selecting a zero-length block just after the hidden part.
So either you have to go back in the rich edit buffer and scan for the link; or use another method to retrieve the clicked text.
In my case, as I have small texts only in the rich edit, I've used the WM_GETTEXT. It returns a plain-text version of the rich edit document, but with the friendly name hyperlinks preserved in this form:
HYPERLINK "https://www.example.com" Example
The CHARRANGE points to the URL, strangely including the leading quote: ("https://www.example.com).
But the indexes correspond to a text with a single-character (LF) line separators. While the WM_GETTEXT returns the CRLF separators. So you have to convert the text to the LF before extracting the URL using the CHARRANGE.
According to the documentation of EM_AUTOURLDETECT, you are supposed to get an EN_LINK notification, which should be reflected in the nmhdr.code. According to Google,
#define EN_LINK 0x70B
which is 7 * 256 + 11 = 1750 + 42 + 11 = 1803.
Please note that your code misses a check for nmhdr.code == EN_LINK.
I'm not sure if the control sends NM_HOVER messages at all.