WPF Image Pan, Zoom e Scroll com camadas em uma tela
Espero que alguém possa me ajudar aqui. Eu estou construindo um aplicativo de imagem WPF que leva imagens ao vivo de uma câmera permitindo aos usuários visualizar a imagem e, posteriormente, destacar regiões de interesse (ROI) nessa imagem. As informações sobre as ROIs (largura, altura, localização em relação a um ponto da imagem, etc.) são enviadas de volta para a câmera, informando / treinando o firmware da câmera onde procurar por códigos de barras, texto, níveis de líquido, voltas num parafuso, etc. na imagem). Um recurso desejado é a capacidade de deslocar e ampliar a imagem e suas ROIs, além de rolar quando a imagem é ampliada com um zoom maior que a área de exibição. O StrokeThickness e o FontSize dos ROIs precisam manter a escala original, mas a largura e a altura das formas dentro de uma ROI precisam ser dimensionadas com a imagem (isso é crítico para capturar locais de pixel exatos para transmitir para a câmera). Eu tenho mais isso funcionou com a exceção de rolagem e alguns outros problemas. Minhas duas áreas de preocupação são:
Quando introduzo um ScrollViewer, não obtenho nenhum comportamento de rolagem. Pelo que entendi, preciso introduzir um LayoutTransform para obter o comportamento correto do ScrollViewer. No entanto, quando faço isso, outras áreas começam a falhar (por exemplo, as ROIs não mantêm a posição correta sobre a imagem ou o ponteiro do mouse começa a se mover para longe do ponto selecionado na imagem ou no canto esquerdo da imagem. salta para a posição atual do mouse em MouseDown.)
Eu não consigo obter o dimensionamento do meu ROI da maneira que eu preciso deles. Eu tenho esse trabalho, mas não é ideal. O que eu tenho não retém a espessura exata do traço, e eu não olhei para ignorar a escala nos blocos de texto. Espero que você veja o que estou fazendo nas amostras de código.
Tenho certeza de que meu problema tem algo a ver com minha falta de compreensão das Transformações e sua relação com o sistema de layout do WPF. Espero que uma versão do código que exibe o que eu realizei até agora ajude (veja abaixo).
FYI, se Adorners é a sugestão, que pode não funcionar no meu cenário, porque eu poderia acabar com mais adornos do que são suportados (rumores 144 adornos é quando as coisas começam a quebrar).
Primeiro, abaixo, uma captura de tela mostrando uma imagem com o ROI (texto e uma forma). O retângulo, a elipse e o texto precisam seguir a área da imagem em escala e rotação, mas não devem ser dimensionados ou redimensionados.
Aqui está o XAML que está mostrando a imagem acima, junto com um Slider para zoom (o zoom da roda do mouse virá mais tarde)
<Window x:Class="PanZoomStackOverflow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="MainWindow" Height="768" Width="1024">
<DockPanel>
<Slider x:Name="_ImageZoomSlider" DockPanel.Dock="Bottom"
Value="2"
HorizontalAlignment="Center" Margin="6,0,0,0"
Width="143" Minimum=".5" Maximum="20" SmallChange=".1"
LargeChange=".2" TickFrequency="2"
TickPlacement="BottomRight" Padding="0" Height="23"/>
<!-- This resides in a user control in my solution -->
<Grid x:Name="LayoutRoot">
<ScrollViewer Name="border" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Grid x:Name="_ImageDisplayGrid">
<Image x:Name="_DisplayImage" Margin="2" Stretch="None"
Source="Untitled.bmp"
RenderTransformOrigin ="0.5,0.5"
RenderOptions.BitmapScalingMode="NearestNeighbor"
MouseLeftButtonDown="ImageScrollArea_MouseLeftButtonDown"
MouseLeftButtonUp="ImageScrollArea_MouseLeftButtonUp"
MouseMove="ImageScrollArea_MouseMove">
<Image.LayoutTransform>
<TransformGroup>
<ScaleTransform />
<TranslateTransform />
</TransformGroup>
</Image.LayoutTransform>
</Image>
<AdornerDecorator> <!-- Using this Adorner Decorator for Move, Resize and Rotation and feedback adornernments -->
<Canvas x:Name="_ROICollectionCanvas"
Width="{Binding ElementName=_DisplayImage, Path=ActualWidth, Mode=OneWay}"
Height="{Binding ElementName=_DisplayImage, Path=ActualHeight, Mode=OneWay}"
Margin="{Binding ElementName=_DisplayImage, Path=Margin, Mode=OneWay}">
<!-- This is a user control in my solution -->
<Grid IsHitTestVisible="False" Canvas.Left="138" Canvas.Top="58" Height="25" Width="186">
<TextBlock Text="Rectangle ROI" HorizontalAlignment="Center" VerticalAlignment="Top"
Foreground="Orange" FontWeight="Bold" Margin="0,-15,0,0"/>
<Rectangle StrokeThickness="2" Stroke="Orange"/>
</Grid>
<!-- This is a user control in my solution -->
<Grid IsHitTestVisible="False" Canvas.Left="176" Canvas.Top="154" Height="65" Width="69">
<TextBlock Text="Ellipse ROI" HorizontalAlignment="Center" VerticalAlignment="Top"
Foreground="Orange" FontWeight="Bold" Margin="0,-15,0,0"/>
<Ellipse StrokeThickness="2" Stroke="Orange"/>
</Grid>
</Canvas>
</AdornerDecorator>
</Grid>
</ScrollViewer>
</Grid>
</DockPanel>
Aqui está o C # que gerencia pan e zoom.
public partial class MainWindow : Window
{
private Point origin;
private Point start;
private Slider _slider;
public MainWindow()
{
this.InitializeComponent();
//Setup a transform group that we'll use to manage panning of the image area
TransformGroup group = new TransformGroup();
ScaleTransform st = new ScaleTransform();
group.Children.Add(st);
TranslateTransform tt = new TranslateTransform();
group.Children.Add(tt);
//Wire up the slider to the image for zooming
_slider = _ImageZoomSlider;
_slider.ValueChanged += _ImageZoomSlider_ValueChanged;
st.ScaleX = _slider.Value;
st.ScaleY = _slider.Value;
//_ImageScrollArea.RenderTransformOrigin = new Point(0.5, 0.5);
//_ImageScrollArea.LayoutTransform = group;
_DisplayImage.RenderTransformOrigin = new Point(0.5, 0.5);
_DisplayImage.RenderTransform = group;
_ROICollectionCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
_ROICollectionCanvas.RenderTransform = group;
}
//Captures the mouse to prepare for panning the scrollable image area
private void ImageScrollArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_DisplayImage.ReleaseMouseCapture();
}
//Moves/Pans the scrollable image area assuming mouse is captured.
private void ImageScrollArea_MouseMove(object sender, MouseEventArgs e)
{
if (!_DisplayImage.IsMouseCaptured) return;
var tt = (TranslateTransform)((TransformGroup)_DisplayImage.RenderTransform).Children.First(tr => tr is TranslateTransform);
Vector v = start - e.GetPosition(border);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
}
//Cleanup for Move/Pan when mouse is released
private void ImageScrollArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_DisplayImage.CaptureMouse();
var tt = (TranslateTransform)((TransformGroup)_DisplayImage.RenderTransform).Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(border);
origin = new Point(tt.X, tt.Y);
}
//Zoom according to the slider changes
private void _ImageZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
//Panel panel = _ImageScrollArea;
Image panel = _DisplayImage;
//Set the scale coordinates on the ScaleTransform from the slider
ScaleTransform transform = (ScaleTransform)((TransformGroup)panel.RenderTransform).Children.First(tr => tr is ScaleTransform);
transform.ScaleX = _slider.Value;
transform.ScaleY = _slider.Value;
//Set the zoom (this will affect rotate too) origin to the center of the panel
panel.RenderTransformOrigin = new Point(0.5, 0.5);
foreach (UIElement child in _ROICollectionCanvas.Children)
{
//Assume all shapes are contained in a panel
Panel childPanel = child as Panel;
var x = childPanel.Children;
//Shape width and heigh should scale, but not StrokeThickness
foreach (var shape in childPanel.Children.OfType<Shape>())
{
if (shape.Tag == null)
{
//Hack: This is be a property on a usercontrol in my solution
shape.Tag = shape.StrokeThickness;
}
double orignalStrokeThickness = (double)shape.Tag;
//Attempt to keep the underlying shape border/stroke from thickening as well
double newThickness = shape.StrokeThickness - (orignalStrokeThickness / transform.ScaleX);
shape.StrokeThickness -= newThickness;
}
}
}
}
O código deve funcionar em um projeto e solução .NET 4.0 ou 4.5, assumindo que não há erros de recorte / colagem.
Alguma ideia? Sugestões são bem vindas.