Hello @mc ,
In Windows Forms, the displayed text is typically managed through the control’s Text property. The Button control overview also describes the button as a Windows Forms control with its own managed behavior. By contrast, WM_SETTEXT is a Win32 message used to set the text of a native window/control. Because of this, in a WinForms app there can be a difference between the native window text and the managed WinForms state.
I tested a small WinForms reproduce like this:
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
namespace WinFormsButtonLab
{
public partial class Form1 : Form
{
private Button targetButton;
private Button btnChangeByProperty;
private Button btnChangeBySendMessage;
private Button btnShowCurrentText;
private TextBox logBox;
private const int WM_SETTEXT = 0x000C;
private const int WM_GETTEXT = 0x000D;
private const int WM_GETTEXTLENGTH = 0x000E;
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, string lParam);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, StringBuilder lParam);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
public Form1()
{
InitializeComponent();
BuildLabUi();
}
private void BuildLabUi()
{
this.Text = "WinForms Button Lab";
this.Width = 700;
this.Height = 400;
targetButton = new Button
{
Left = 30,
Top = 30,
Width = 200,
Height = 50,
Text = "Original Text"
};
btnChangeByProperty = new Button
{
Left = 30,
Top = 100,
Width = 200,
Height = 40,
Text = "Change by Property"
};
btnChangeByProperty.Click += (s, e) =>
{
targetButton.Text = "Changed by Property";
targetButton.Refresh();
Log("Changed targetButton.Text directly.");
DumpState("After property change");
};
btnChangeBySendMessage = new Button
{
Left = 250,
Top = 100,
Width = 220,
Height = 40,
Text = "Change by SendMessage"
};
btnChangeBySendMessage.Click += (s, e) =>
{
SendMessage(targetButton.Handle, WM_SETTEXT, IntPtr.Zero, "Changed by WM_SETTEXT");
targetButton.Refresh();
Log("Sent WM_SETTEXT to targetButton.Handle.");
DumpState("After SendMessage");
};
btnShowCurrentText = new Button
{
Left = 490,
Top = 100,
Width = 150,
Height = 40,
Text = "Show Current Text"
};
btnShowCurrentText.Click += (s, e) =>
{
DumpState("Manual state check");
};
logBox = new TextBox
{
Left = 30,
Top = 170,
Width = 610,
Height = 150,
Multiline = true,
ScrollBars = ScrollBars.Vertical
};
targetButton.HandleCreated += (s, e) =>
{
Log("HandleCreated: " + targetButton.Handle);
DumpState("After handle created");
};
this.Controls.Add(targetButton);
this.Controls.Add(btnChangeByProperty);
this.Controls.Add(btnChangeBySendMessage);
this.Controls.Add(btnShowCurrentText);
this.Controls.Add(logBox);
}
private void DumpState(string title)
{
Log("---- " + title + " ----");
Log("Managed Text: [" + targetButton.Text + "]");
Log("Native Text : [" + GetNativeText(targetButton.Handle) + "]");
Log("Handle : " + targetButton.Handle);
Log("Visible UI : check button caption manually");
}
private string GetNativeText(IntPtr hwnd)
{
int length = (int)SendMessage(hwnd, WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero);
StringBuilder sb = new StringBuilder(length + 1);
SendMessage(hwnd, WM_GETTEXT, (IntPtr)sb.Capacity, sb);
return sb.ToString();
}
private void Log(string message)
{
logBox.AppendText(message + Environment.NewLine);
}
}
}
In my test, setting targetButton.Text changed the visible caption as expected. However, sending WM_SETTEXT directly to the WinForms button handle did not update the visible button text. The native text changed, but the WinForms button caption did not.

I also tried the following workaround:
btnChangeBySendMessage.Click += (s, e) =>
{
string requestedText = "Changed by WM_SETTEXT";
IntPtr result = SendMessage(targetButton.Handle, WM_SETTEXT, IntPtr.Zero, requestedText);
Log("Sent WM_SETTEXT to targetButton.Handle. Result = " + result);
string nativeText = GetNativeText(targetButton.Handle);
Log("Native text after WM_SETTEXT = [" + nativeText + "]");
// Workaround: sync managed WinForms layer from native text
targetButton.Text = nativeText;
targetButton.Refresh();
Log("Applied workaround: targetButton.Text synchronized from native text.");
DumpState("After SendMessage workaround");
};
With this workaround, the button text was updated in my test.

Hope this clarifies your question. If you have any further questions, please let me know in the comments. If you find the answer helpful, I would appreciate it if you could leave feedback by following this guide.
Thank you.