이 글에 대한 해결책을 얻고자 하신다면 조금 다른 방향으로 시도해 볼 수 있습니다.
WPF에서 사용되는 대부분의 컨트롤(TextBox
의 입력 관련 렌더링이나 FlowDocument
내의 문자들 제외)은 TextBlock
을 통해 문자를 표출하는데요, 이 TextBlock
의 Text
속성에 할당된 문자열을 그리기 위한 내부 객체를 생성하는 부분의 로직을 가로채서 문자열을 바꿔줄 수 있습니다.
이 방법은 무협으로 치면 사파 무공
에 해당하는 방법으로 실무에 사용하는 것은 추천하지 않습니다.
런타임에 이미 컴파일 된 함수를 후킹해서 원하는 기능을 하도록 변경하는 방법으로 정석적인 방법과는 거리가 멀며 잘못 사용할 경우 소프트웨어의 안정성에 큰 영향을 줄 수도 있는 파괴적인 방법입니다.
본론으로 들어가 TextBlock
이 Text
속성 값을 어떻게 그리지는 확인하기 위해 dnSpy를 통해 OnRender
함수를 살펴보면, 위에서 언급한 Text
속성을 그리기 위한 내부 객체는 Line
이라는 형식의 객체로 CreateLine
이라는 함수를 통해 생성됩니다.
해당 함수의 내부 구현은 아래와 같습니다.
TextBlock
이 Inline
, Run
과 같이 복잡한 형식의 하위 항목을 포함하지 않고 단순히 Text
문자열 만을 포함하고 있다면 SimpleLine
이라는 객체를 만들며, 이때 생성자에 Text
속성 값을 넘기도록 되어 있습니다.
line = new SimpleLine(this, this.Text, lineProperties.DefaultTextRunProperties);
우리는 이 지점에서 this.Text
부분에 대한 조건 검사를 수행한 뒤 변경된 문자열 값을 넘겨줄 것입니다.
런타임 함수를 후킹하기 위해서 프로젝트에 MonoMod.RuntimeDetour
패키지를 설치해 줍니다.
프로그램 진입점인 App
클래스에 다음 내용을 추가해 줍니다.
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using MonoMod.RuntimeDetour;
public partial class App : Application
{
private static Hook _hookCreateLine; // Keep hook object from GC
private static PropertyInfo _piDefaultTextRunProperties;
private static ConstructorInfo _ciSimpleLine;
public App()
{
var miCreateLine = typeof(TextBlock).GetMethod(
"CreateLine",
BindingFlags.NonPublic | BindingFlags.Instance);
_hookCreateLine = new Hook(miCreateLine, CreateLine);
var tiSimpleLine = typeof(TextBlock).Assembly.DefinedTypes
.FirstOrDefault(x => x.Name == "SimpleLine");
_ciSimpleLine = tiSimpleLine.DeclaredConstructors.FirstOrDefault();
}
private static object CreateLine(
Func<TextBlock, object, object> orig, TextBlock self,
object lineProperty)
{
if (_piDefaultTextRunProperties == null)
{
_piDefaultTextRunProperties =
lineProperty.GetType().GetProperty("DefaultTextRunProperties");
}
if (self.Text == "A") // 조건 검사,
{
var trp = _piDefaultTextRunProperties.GetValue(lineProperty);
var replaced = "A11"; // 변경된 값
// SimpleLine 생성 후 리턴
return _ciSimpleLine.Invoke(new object[] { self, replaced, trp });
}
else // 조건에 부합하지 않으면 원본 메서드를 호출
{
return orig(self, lineProperty);
}
}
}
아래와 같은 결과를 얻으실 수 있습니다.
관련 패키지에 대해서는 아래 링크를 참고하시면 도움이 될 듯 합니다.
추가적으로 위와 같은 변경이 필요한 컨트롤은 후킹 지점을 직접 찾아 구현해 보시기 바랍니다.
MonoMod/MonoMod.UnitTest/RuntimeDetour/HookTest.cs at master · MonoMod/MonoMod (github.com)