쿼리 편집기 세 가지 — 한글이 사라지던 것, 줄 번호가 밀리던 것, 클릭이 먹지 않던 것
1) 한글로 입력하면 글자가 안 보였다. 색칠은 "투명한 TextBox 뒤에 우리가 그린다"로 되어 있다. 그런데 IME 조합 중인 글자는 아직 TextBox.Text 에 없다 — TextChanged 가 오지 않으니 우리는 그릴 수 없고, TextBox 는 제 Foreground(투명)로 그리니 아무것도 안 보인다. 자음·모음을 맞추는 내내 빈 화면이었고, 확정하고 나서야 나타났다. 조합이 시작되면 TextBox 에게 잉크색을 돌려주고 색칠 층을 감춘다(ShowRawText). 확정되면(TextInput) 되돌리고, ESC 로 취소돼 CompositionText 가 비어도 되돌린다. 조합 도중 포커스를 잃으면 끝 신호가 오지 않으므로 blur 에서도 되돌린다 — 색 없는 상태로 굳지 않게. 조합 중 잠깐 단색이 되는 편이 글자가 안 보이는 것보다 낫다. 2) 줄 번호가 실제 줄과 어긋났다. 번호를 "1\n2\n3…" TextBlock 한 덩어리로 두면 번호의 줄 간격은 TextBlock 조판이, 본문의 줄 간격은 TextBox 가 정한다. 한글 글꼴 대체가 끼면 조금씩 벌어져 아래로 갈수록 눈에 띄게 밀렸다. LineNumberGutter 를 만들어 각 줄 첫 글자의 y 를 GetRectFromCharacterIndex 로 물어 그 자리에 번호를 그린다. 스크롤·여백·폰트 대체가 전부 TextBox 기준으로 반영돼 오므로 구조적으로 어긋날 수 없다. 폭은 가장 큰 번호에 맞춰 잡고 번호는 오른쪽 정렬한다. 3) 자동완성 목록의 항목을 클릭하면 아무것도 안 들어가고 목록만 닫혔다. Popup 이 StaysOpen=False 라 마우스를 캡처하고 있었다. 항목을 누른 그 클릭이 '바깥 클릭'으로 먼저 소비돼 팝업이 닫히고, 항목은 클릭을 받지 못했다. StaysOpen=True 로 바꾸고 닫는 일은 코드가 맡는다(커서 이동·포커스 상실·창 비활성 — 이미 있던 핸들러들). 집는 처리는 팝업 안쪽 Border 에서 터널 단계로 받는다. 진단(--query-popup)에 "목록 항목을 집으면 입력되고 목록이 닫힌다"를 더해 17건. 마우스 라우팅 자체는 화면 없이 흉내 낼 수 없어(Ctrl+Enter 때와 같은 이유) 핸들러가 부르는 경로를 그대로 태워 삽입까지 확인한다. IME 조합은 합성할 수 없어 1) 은 실기 확인이 필요하다. 게이트: 테스트 230/230, --edit-smoke 0건, --db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6ce055b00c
commit
749de6b566
@@ -0,0 +1,174 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SheetMe.Designer.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 줄 번호 홈통 — 번호를 <b>TextBox 가 알려 준 줄 위치</b>에 그린다.
|
||||
///
|
||||
/// <b>왜 TextBlock 한 줄로 두면 안 되는가.</b> 예전에는 "1\n2\n3…" 을 TextBlock 에 넣어 두었다.
|
||||
/// 그러면 번호의 줄 간격은 TextBlock 의 조판이 정하고 본문의 줄 간격은 TextBox 가 정한다 —
|
||||
/// 글꼴 대체(한글)나 여백이 조금만 달라도 아래로 갈수록 벌어져 번호와 줄이 어긋난다.
|
||||
///
|
||||
/// 여기서는 각 줄의 첫 글자 위치를 <see cref="TextBox.GetRectFromCharacterIndex(int)"/> 로 물어
|
||||
/// 그 y 에 번호를 그린다. 스크롤·여백·글꼴 대체가 전부 TextBox 기준으로 반영돼 오므로 어긋날 수 없다.
|
||||
/// </summary>
|
||||
public sealed class LineNumberGutter : FrameworkElement
|
||||
{
|
||||
#region Member Fields
|
||||
private TextBox? source;
|
||||
private ScrollViewer? scroller;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>줄 위치를 물어볼 대상</summary>
|
||||
public TextBox? Source
|
||||
{
|
||||
get => source;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(source, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Detach();
|
||||
source = value;
|
||||
Attach();
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>오른쪽 여백 — 구분선과 번호 사이</summary>
|
||||
public double RightPadding { get; set; } = 10;
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void Attach()
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
source.TextChanged += OnChanged;
|
||||
source.SizeChanged += OnChanged;
|
||||
source.Loaded += OnLoaded;
|
||||
HookScroller();
|
||||
}
|
||||
|
||||
private void Detach()
|
||||
{
|
||||
if (source is not null)
|
||||
{
|
||||
source.TextChanged -= OnChanged;
|
||||
source.SizeChanged -= OnChanged;
|
||||
source.Loaded -= OnLoaded;
|
||||
}
|
||||
if (scroller is not null)
|
||||
{
|
||||
scroller.ScrollChanged -= OnScrolled;
|
||||
scroller = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HookScroller();
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
private void HookScroller()
|
||||
{
|
||||
if (source is null || scroller is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
scroller = FindScrollViewer(source);
|
||||
if (scroller is not null)
|
||||
{
|
||||
scroller.ScrollChanged += OnScrolled;
|
||||
}
|
||||
}
|
||||
|
||||
private static ScrollViewer? FindScrollViewer(DependencyObject root)
|
||||
{
|
||||
if (root is ScrollViewer found)
|
||||
{
|
||||
return found;
|
||||
}
|
||||
var count = VisualTreeHelper.GetChildrenCount(root);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (FindScrollViewer(VisualTreeHelper.GetChild(root, i)) is { } child)
|
||||
{
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnScrolled(object sender, ScrollChangedEventArgs e) => InvalidateVisual();
|
||||
|
||||
private void OnChanged(object sender, EventArgs e) => InvalidateVisual();
|
||||
|
||||
/// <summary>가장 큰 번호가 들어갈 만큼만 폭을 잡는다 — 줄이 늘면 홈통도 넓어진다</summary>
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
return new Size(28, 0);
|
||||
}
|
||||
var lines = 1;
|
||||
foreach (var ch in source.Text)
|
||||
{
|
||||
if (ch == '\n')
|
||||
{
|
||||
lines++;
|
||||
}
|
||||
}
|
||||
var probe = Measure(lines.ToString(CultureInfo.InvariantCulture));
|
||||
return new Size(Math.Max(20, probe.Width) + RightPadding, availableSize.Height);
|
||||
}
|
||||
|
||||
private FormattedText Measure(string text)
|
||||
=> new(text, CultureInfo.InvariantCulture, FlowDirection.LeftToRight,
|
||||
new Typeface(source!.FontFamily, source.FontStyle, source.FontWeight, source.FontStretch),
|
||||
source.FontSize, Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip);
|
||||
|
||||
protected override void OnRender(DrawingContext dc)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var text = source.Text;
|
||||
var brush = TryFindResource("B.Muted") as Brush ?? Brushes.Gray;
|
||||
var height = ActualHeight;
|
||||
|
||||
var lineIndex = 0;
|
||||
var lineStart = 0;
|
||||
while (true)
|
||||
{
|
||||
var rect = source.GetRectFromCharacterIndex(Math.Min(lineStart, text.Length));
|
||||
if (!rect.IsEmpty && !double.IsInfinity(rect.Top) && !double.IsNaN(rect.Top)
|
||||
&& rect.Bottom >= 0 && rect.Top <= height)
|
||||
{
|
||||
var label = Measure((lineIndex + 1).ToString(CultureInfo.InvariantCulture));
|
||||
label.SetForegroundBrush(brush);
|
||||
// 오른쪽 정렬 — 자릿수가 늘어도 번호 끝이 구분선에서 같은 거리를 유지한다
|
||||
dc.DrawText(label, new Point(ActualWidth - RightPadding - label.Width, rect.Y));
|
||||
}
|
||||
|
||||
var newline = text.IndexOf('\n', lineStart);
|
||||
if (newline < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
lineStart = newline + 1;
|
||||
lineIndex++;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user