레이어 패널: 컨트롤 이름 병기 + 더블클릭으로 캔버스 이동
레이어 목록이 Text 만 보여줘서 chkOrientation1~5 처럼 문구가 같은 컨트롤들이 전부 같은 줄로 보였다. 두 줄로 나눠 아래에 Id 를 병기한다 (Text 가 없어 LayerName 이 곧 Id 인 경우는 중복이라 숨긴다). 그리고 컨트롤이 수백 개인 서식에서는 목록에서 이름을 찾아도 종이 어디에 있는지 알 수 없었다 — 더블클릭하면 해당 페이지로 전환 + 선택 + 캔버스 스크롤. 이미 전부 보이면 스크롤하지 않는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e18ad87100
commit
d09bd90c8e
@@ -227,6 +227,8 @@ public static class DialogShots
|
|||||||
page.Controls.Add(NewControl("Label", "Label1", "환자명", 40, 40, 120, 24));
|
page.Controls.Add(NewControl("Label", "Label1", "환자명", 40, 40, 120, 24));
|
||||||
page.Controls.Add(NewControl("TextBox", "TextBox1", string.Empty, 170, 40, 200, 26));
|
page.Controls.Add(NewControl("TextBox", "TextBox1", string.Empty, 170, 40, 200, 26));
|
||||||
page.Controls.Add(NewControl("Label", "Label2", "생년월일", 40, 80, 120, 24));
|
page.Controls.Add(NewControl("Label", "Label2", "생년월일", 40, 80, 120, 24));
|
||||||
|
// 이름만 있고 문구가 없는 컨트롤 — 레이어 목록에서 이름 칸이 중복 표시되지 않는지 확인용
|
||||||
|
page.Controls.Add(NewControl("TextBox", "TextBox2", string.Empty, 170, 80, 200, 26));
|
||||||
document.Pages.Add(page);
|
document.Pages.Add(page);
|
||||||
return new DesignerViewModel(document);
|
return new DesignerViewModel(document);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,8 +137,7 @@ public abstract class ControlViewModel : ViewModelBase
|
|||||||
/// 레이어 목록 표시명 — Text 가 있으면 그것, 없으면 Id.
|
/// 레이어 목록 표시명 — Text 가 있으면 그것, 없으면 Id.
|
||||||
///
|
///
|
||||||
/// Id 는 대부분 <c>Label12</c> 처럼 자동 생성된 이름이라 목록에서 아무것도 알려주지 않는다.
|
/// Id 는 대부분 <c>Label12</c> 처럼 자동 생성된 이름이라 목록에서 아무것도 알려주지 않는다.
|
||||||
/// 다만 Id 는 외부 참조 키이기도 하므로(DataTableField 대상명, EventHandlerMappingTag 의
|
/// 그래서 앞줄은 사람이 읽는 문구로 두고, 이름은 <see cref="LayerId"/> 로 따로 보여준다.
|
||||||
/// <c>ShtCod_컨트롤명</c> 규약) 버리지 않고 툴팁·검색 대상으로 함께 유지한다.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string LayerName
|
public string LayerName
|
||||||
{
|
{
|
||||||
@@ -153,6 +152,21 @@ public abstract class ControlViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 레이어 목록에 함께 보여줄 컨트롤 이름(Id).
|
||||||
|
///
|
||||||
|
/// 이름은 <b>외부 참조 키</b>다 — 액션 대상 컨트롤·입력 파라미터 컨트롤이 이름 문자열로
|
||||||
|
/// 서로를 가리키고, CalcBox 수식은 <c>RadioButton1+CheckBox3</c> 처럼 이름으로 합산하며,
|
||||||
|
/// EventHandlerMappingTag 는 <c>ShtCod_컨트롤명</c> 규약을 쓴다. 배선을 하려면 눈에 보여야 한다.
|
||||||
|
///
|
||||||
|
/// 표시할 문구가 없어 <see cref="LayerName"/> 이 이미 Id 인 경우에는 빈 값을 돌려준다 —
|
||||||
|
/// 같은 값을 한 줄에 두 번 쓰면 좁은 행에서 이름 칸만 잡아먹는다.
|
||||||
|
/// </summary>
|
||||||
|
public string LayerId => LayerName == Id ? string.Empty : Id;
|
||||||
|
|
||||||
|
/// <summary>이름 칸을 그릴지 — 문구와 이름이 다를 때만</summary>
|
||||||
|
public bool HasLayerId => LayerId.Length > 0;
|
||||||
|
|
||||||
/// <summary>레이어 잠금 토글 아이콘명</summary>
|
/// <summary>레이어 잠금 토글 아이콘명</summary>
|
||||||
public string LockIcon => Model.Locked ? "lock" : "unlock";
|
public string LockIcon => Model.Locked ? "lock" : "unlock";
|
||||||
|
|
||||||
|
|||||||
@@ -621,6 +621,30 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
#region Events
|
#region Events
|
||||||
/// <summary>인라인 텍스트 편집 요청 — 캔버스 뷰(EditorLayer)가 구독</summary>
|
/// <summary>인라인 텍스트 편집 요청 — 캔버스 뷰(EditorLayer)가 구독</summary>
|
||||||
public event Action<ControlViewModel>? InlineEditRequested;
|
public event Action<ControlViewModel>? InlineEditRequested;
|
||||||
|
|
||||||
|
/// <summary>캔버스를 특정 컨트롤로 스크롤 요청 — 캔버스 뷰가 구독(뷰포트를 VM 이 모르므로 위임)</summary>
|
||||||
|
public event Action<ControlViewModel>? ScrollToRequested;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Methods - 컨트롤로 이동
|
||||||
|
/// <summary>
|
||||||
|
/// 레이어 목록에서 고른 컨트롤을 선택하고 화면을 그리로 옮긴다.
|
||||||
|
///
|
||||||
|
/// 컨트롤이 수백 개인 서식(운영 최대 1,067개)에서는 목록에서 이름을 찾아도 그게 종이 어디에
|
||||||
|
/// 있는지 알 수 없었다. 다중 페이지 서식이면 다른 페이지에 있을 수도 있어 더 그렇다.
|
||||||
|
/// 스크롤은 뷰포트 크기를 알아야 하므로 캔버스 뷰에 위임한다.
|
||||||
|
/// </summary>
|
||||||
|
public void RevealControl(ControlViewModel viewModel)
|
||||||
|
{
|
||||||
|
// 다른 페이지의 컨트롤이면 그 페이지를 활성으로 — 안 그러면 선택만 되고 화면은 안 따라간다
|
||||||
|
var page = PageOf(viewModel);
|
||||||
|
if (page is not null && !ReferenceEquals(page, SelectedPage))
|
||||||
|
{
|
||||||
|
SelectedPage = page;
|
||||||
|
}
|
||||||
|
Selection.SetSingle(viewModel);
|
||||||
|
ScrollToRequested?.Invoke(viewModel);
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Methods - 인라인 텍스트 편집
|
#region Methods - 인라인 텍스트 편집
|
||||||
|
|||||||
@@ -40,16 +40,60 @@ public partial class DesignerCanvasView : UserControl
|
|||||||
if (subscribedDesigner is not null)
|
if (subscribedDesigner is not null)
|
||||||
{
|
{
|
||||||
subscribedDesigner.InlineEditRequested -= ShowInlineEditor;
|
subscribedDesigner.InlineEditRequested -= ShowInlineEditor;
|
||||||
|
subscribedDesigner.ScrollToRequested -= ScrollToControl;
|
||||||
}
|
}
|
||||||
CloseInlineEditor(commit: false);
|
CloseInlineEditor(commit: false);
|
||||||
subscribedDesigner = e.NewValue as DesignerViewModel;
|
subscribedDesigner = e.NewValue as DesignerViewModel;
|
||||||
if (subscribedDesigner is not null)
|
if (subscribedDesigner is not null)
|
||||||
{
|
{
|
||||||
subscribedDesigner.InlineEditRequested += ShowInlineEditor;
|
subscribedDesigner.InlineEditRequested += ShowInlineEditor;
|
||||||
|
subscribedDesigner.ScrollToRequested += ScrollToControl;
|
||||||
subscribedDesigner.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight);
|
subscribedDesigner.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 컨트롤이 화면 가운데 오도록 스크롤 — 레이어 목록 더블클릭 경로.
|
||||||
|
///
|
||||||
|
/// 월드 좌표 → 화면 좌표 변환은 World 그리드의 LayoutTransform(줌)과 감싸는 Border 의
|
||||||
|
/// Padding 24 를 모두 반영해야 한다. 이미 보이는 컨트롤은 건드리지 않는다 —
|
||||||
|
/// 목록을 훑을 때마다 화면이 튀면 오히려 방향 감각을 잃는다.
|
||||||
|
/// </summary>
|
||||||
|
private void ScrollToControl(ControlViewModel target)
|
||||||
|
{
|
||||||
|
if (subscribedDesigner is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 페이지 전환·선택 변경이 레이아웃에 반영된 뒤 계산해야 오프셋이 맞는다
|
||||||
|
Dispatcher.BeginInvoke(new Action(() =>
|
||||||
|
{
|
||||||
|
var bounds = subscribedDesigner.WorldBoundsOf(target);
|
||||||
|
var zoom = subscribedDesigner.Zoom;
|
||||||
|
const double CanvasPad = 24; // DesignerCanvasView.xaml 의 Border Padding
|
||||||
|
|
||||||
|
var left = bounds.X * zoom + CanvasPad;
|
||||||
|
var top = bounds.Y * zoom + CanvasPad;
|
||||||
|
var width = bounds.Width * zoom;
|
||||||
|
var height = bounds.Height * zoom;
|
||||||
|
|
||||||
|
var visibleLeft = Scroll.HorizontalOffset;
|
||||||
|
var visibleTop = Scroll.VerticalOffset;
|
||||||
|
var visibleRight = visibleLeft + Scroll.ViewportWidth;
|
||||||
|
var visibleBottom = visibleTop + Scroll.ViewportHeight;
|
||||||
|
|
||||||
|
var fullyVisible = left >= visibleLeft && top >= visibleTop
|
||||||
|
&& left + width <= visibleRight && top + height <= visibleBottom;
|
||||||
|
if (fullyVisible)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scroll.ScrollToHorizontalOffset(left + (width / 2) - (Scroll.ViewportWidth / 2));
|
||||||
|
Scroll.ScrollToVerticalOffset(top + (height / 2) - (Scroll.ViewportHeight / 2));
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>뷰포트 크기를 VM 에 전달 — 화면 맞춤(ZoomFit)이 시각 트리를 몰라도 되게 한다</summary>
|
/// <summary>뷰포트 크기를 VM 에 전달 — 화면 맞춤(ZoomFit)이 시각 트리를 몰라도 되게 한다</summary>
|
||||||
private void OnScrollSizeChanged(object sender, SizeChangedEventArgs e)
|
private void OnScrollSizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
=> subscribedDesigner?.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight);
|
=> subscribedDesigner?.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight);
|
||||||
|
|||||||
@@ -529,8 +529,10 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 레이어 목록 (활성 페이지, 그리기 순서) -->
|
<!-- 레이어 목록 (활성 페이지, 그리기 순서) -->
|
||||||
<ListBox ItemsSource="{Binding CurrentDesigner.VisibleLayers}"
|
<ListBox x:Name="LayerListBox" ItemsSource="{Binding CurrentDesigner.VisibleLayers}"
|
||||||
SelectedItem="{Binding CurrentDesigner.SelectedLayerItem}"
|
SelectedItem="{Binding CurrentDesigner.SelectedLayerItem}"
|
||||||
|
MouseDoubleClick="OnLayerDoubleClick"
|
||||||
|
ToolTip="더블클릭하면 캔버스가 그 컨트롤로 이동합니다"
|
||||||
Margin="4,0,4,6"
|
Margin="4,0,4,6"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
@@ -545,18 +547,31 @@
|
|||||||
Content="{Binding HiddenIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/>
|
Content="{Binding HiddenIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/>
|
||||||
<ContentControl DockPanel.Dock="Left" Margin="2,0,6,0" VerticalAlignment="Center" IsTabStop="False"
|
<ContentControl DockPanel.Dock="Left" Margin="2,0,6,0" VerticalAlignment="Center" IsTabStop="False"
|
||||||
Content="{Binding Type, Converter={StaticResource TypeToIcon}, ConverterParameter=14|B.Accent}"/>
|
Content="{Binding Type, Converter={StaticResource TypeToIcon}, ConverterParameter=14|B.Accent}"/>
|
||||||
<TextBlock Text="{Binding LayerName}" FontSize="12" VerticalAlignment="Center"
|
<!--
|
||||||
TextTrimming="CharacterEllipsis" ToolTip="{Binding Id}">
|
문구와 이름을 두 줄로. 컨트롤 이름(Id)은 외부 참조 키라
|
||||||
<TextBlock.Style>
|
(액션 대상·입력 파라미터가 이름으로 서로를 가리키고 CalcBox 수식도 이름으로 합산한다)
|
||||||
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
|
배선하려면 목록에서 보여야 하는데, 248px 폭에 나란히 두면 양쪽 다 잘려
|
||||||
|
'txtKinPhone…' 처럼 구분이 안 되는 꼬리가 남는다. 한 줄 더 쓰는 편이 낫다.
|
||||||
|
표시 문구가 없어 이름이 곧 문구인 행에서는 둘째 줄이 뜨지 않는다.
|
||||||
|
-->
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<StackPanel.Style>
|
||||||
|
<Style TargetType="StackPanel">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding IsHiddenFlag}" Value="True">
|
<DataTrigger Binding="{Binding IsHiddenFlag}" Value="True">
|
||||||
<Setter Property="Opacity" Value="0.55"/>
|
<Setter Property="Opacity" Value="0.55"/>
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</TextBlock.Style>
|
</StackPanel.Style>
|
||||||
</TextBlock>
|
<TextBlock Text="{Binding LayerName}" FontSize="12"
|
||||||
|
TextTrimming="CharacterEllipsis" ToolTip="{Binding LayerName}"/>
|
||||||
|
<TextBlock Text="{Binding LayerId}" FontSize="10.5" Margin="0,1,0,0"
|
||||||
|
Foreground="{DynamicResource B.Muted}"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
ToolTip="컨트롤 이름 — 다른 속성이 이 이름으로 참조합니다"
|
||||||
|
Visibility="{Binding HasLayerId, Converter={StaticResource BoolToVisibility}}"/>
|
||||||
|
</StackPanel>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
|
|||||||
@@ -38,6 +38,20 @@ public partial class MainView : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 레이어 더블클릭 → 캔버스를 그 컨트롤로 이동.
|
||||||
|
/// 컨트롤이 수백 개인 서식에서는 목록에서 이름을 찾아도 종이 어디에 있는지 알 수 없었다.
|
||||||
|
/// (ListBox 더블클릭은 InputBinding 을 지원하지 않아 코드비하인드로 위임한다 — 서식 목록과 같은 이유)
|
||||||
|
/// </summary>
|
||||||
|
private void OnLayerDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is MainViewModel { CurrentDesigner: { } designer }
|
||||||
|
&& LayerListBox.SelectedItem is ViewModels.Controls.ControlViewModel target)
|
||||||
|
{
|
||||||
|
designer.RevealControl(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
|
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ThemeManager.Toggle();
|
ThemeManager.Toggle();
|
||||||
|
|||||||
Reference in New Issue
Block a user