試したところ、.NetFrameworkのバージョンを4.7よりも前に戻すと想定したような文字単位の折り返しになりました。
どうも4.7でRichTextBoxのライブラリがRichEd20.dllではなくMsftEdit.dllを利用するように変更されたらしく、挙動が変わっているみたいです。
いちおう、互換のためのフラグがあるので、設定することで文字単位の折り返しになりました。
app.configで設定する場合
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7"/></startup>
<runtime>
<AppContextSwitchOverrides value="Switch.System.Windows.Forms.DoNotLoadLatestRichEditControl=true"/>
</runtime>
</configuration>
または、コードで設定する場合
namespace CSForm48
{
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
public partial class Form1 : Form
{
public Form1()
{
//RichTextBoxが作成されるよりも前にフラグを設定しておく
//RichTextBoxWrap.TrySetDoNotLoadLatestRichEditControlFlag(true);
RichTextBox rtb = new RichTextBox(); //new RichTextBoxOld();
rtb.Multiline = true;
rtb.Dock = DockStyle.Fill;
rtb.Font = new Font(this.Font.FontFamily, 20);
this.Controls.Add(rtb);
this.ClientSize = new Size(300, 300);
rtb.Text = "あ" + string.Join(" ", "ABCDEFGHIJKLMN".Select(c => new string(c, 5)));
RichTextBoxWrap.SetDefaultWordbreak(rtb);
Timer t = new Timer();
t.Interval = 100;
t.Tick += (s, e) =>
{
int diff = (DateTime.Now.Second < 30) ? 1 : -1;
this.Width = Math.Max(100, Math.Min(this.Width + diff, 600));
};
t.Start();
}
}
class RichTextBoxWrap
{
/// <summary>.Net Framwork4.7以降のRichTextBoxの挙動を以前のに戻すフラグをセットする</summary>
/// <remarks>RichTextBoxが作成されるよりも前にフラグを設定しておく必要がある</remarks>
public static void TrySetDoNotLoadLatestRichEditControlFlag(bool enable = true)
{
//4.6よりも前はビルド不可
string versionName = typeof(System.AppContext).GetProperty("TargetFrameworkName")?.GetValue(null).ToString() ?? "";
string sver = System.Text.RegularExpressions.Regex.Match(versionName, @"\d+(\.\d+)+").Value;
if (System.Version.TryParse(sver, out System.Version v))
{
if (new System.Version(4, 7) <= v && v <= new System.Version(5, 0))
{
System.AppContext.SetSwitch("Switch.System.Windows.Forms.DoNotLoadLatestRichEditControl", enable);
}
}
}
/// <summary>単語区切りの折り返しを無効にして、文字単位の折り返しにする</summary>
/// <param name="rtb"></param>
/// <param name="disable">true:文字単位の折り返しに</param>
public static void SetDefaultWordbreak(System.Windows.Forms.RichTextBox rtb, bool disable = true)
{
if (delCallback == null)
{
delCallback = new Win32.EditWordBreakProcDelegate(CallBack);
pCallback = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(delCallback);
}
if (rtb.Handle == IntPtr.Zero)
{
throw new InvalidOperationException();
}
if (disable)
{
Win32.SendMessageW(rtb.Handle, Win32.EM_SETWORDBREAKPROC, System.IntPtr.Zero, pCallback);
Win32.SendMessageW(rtb.Handle, Win32.WM_SIZE, System.IntPtr.Zero, new System.IntPtr(((uint)rtb.Width << 16) | (uint)rtb.Height));
}
else
{
Win32.SendMessageW(rtb.Handle, Win32.EM_SETWORDBREAKPROC, System.IntPtr.Zero, System.IntPtr.Zero);
}
}
private static Win32.EditWordBreakProcDelegate delCallback;
private static System.IntPtr pCallback = System.IntPtr.Zero;
//----------------------------
private static int CallBack(System.IntPtr text, int ichCurrent, int cch, Win32.WB code)
{
return 0;
//string s = System.Runtime.InteropServices.Marshal.PtrToStringUni(text, cch);
//switch (code)
//{
//case Win32.WB.WB_LEFT:
// return System.Math.Max(0, ichCurrent - 1);
//case Win32.WB.WB_RIGHT:
// return System.Math.Min(ichCurrent + 1, cch);
//case Win32.WB.WB_ISDELIMITER:
// return 0;
//case Win32.WB.WB_CLASSIFY:
// if (char.IsWhiteSpace(s, ichCurrent))
// {
// return (int)Win32.WBF.WBF_ISWHITE;
// }
// else
// {
// return (int)(Win32.WBF.WBF_BREAKAFTER | Win32.WBF.WBF_BREAKLINE);
// }
//case Win32.WB.WB_MOVEWORDLEFT:
//case Win32.WB.WB_LEFTBREAK:
// return System.Math.Max(0, ichCurrent - 1);
//case Win32.WB.WB_MOVEWORDRIGHT:
//case Win32.WB.WB_RIGHTBREAK:
// return System.Math.Min(ichCurrent + 1, cch - 1);
//default:
// return 0;
//}
}
}
class Win32
{
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public extern static System.IntPtr SendMessageW(System.IntPtr hwnd, uint message, System.IntPtr wParam, System.IntPtr lParam);
public const uint WM_SIZE = 5;
public const uint EM_SETWORDBREAKPROC = 0x00D0;
public const uint EM_GETWORDBREAKPROC = 0x00D1;
public delegate int EditWordBreakProcDelegate(System.IntPtr text, int ichCurrent, int cch, WB code);
public enum WB : int
{
WB_LEFT = 0,
WB_RIGHT = 1,
WB_ISDELIMITER = 2,
WB_CLASSIFY = 3,
WB_MOVEWORDLEFT = 4,
WB_MOVEWORDRIGHT = 5,
WB_LEFTBREAK = 6,
WB_RIGHTBREAK = 7,
//// East Asia specific flags
//WB_MOVEWORDPREV = 4,
//WB_MOVEWORDNEXT = 5,
//WB_PREVBREAK = 6,
//WB_NEXTBREAK = 7,
}
public enum WBF : byte
{
WBF_CLASS = 0x0F,
WBF_ISWHITE = 0x10,
WBF_BREAKLINE = 0x20,
WBF_BREAKAFTER = 0x40,
}
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lpFileName);
}
/// <summary>
/// 無理やり古いバージョンを使うRichTextBox
/// </summary>
class RichTextBoxOld : RichTextBox
{
static RichTextBoxOld()
{
string dllPath = System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.System), "riched20.dll");
if (System.IO.File.Exists(dllPath))
{
moduleHandle = Win32.LoadLibrary(dllPath);
}
}
private bool isModleChacked = false;
private static IntPtr moduleHandle=IntPtr.Zero;
protected override CreateParams CreateParams
{
get
{
if (moduleHandle == IntPtr.Zero)
{
return base.CreateParams;
}
CreateParams createParams = base.CreateParams;
createParams.ClassName = "RichEdit20W";
return createParams;
}
}
}
}
ただし、.Net Core以降ではこのフラグが消されてしまったので、互換性がなくなります。
この場合はRichEditコントロールを使わずに、RichEd20.dllを自前でロードして処理するしかないのかも。