How to draw on a zoomed image?
You need to address two issues:
Clip the
Graphics
area to the actualImage
instead of the wholePictureBox.ClientArea
Scale the coordinates of the mouse events to the actual image when receiving and recording them and back again when you use them to draw in the
Paint
event.
For both we need to know the zoom factor of the Image
; for the clipping we also need to know the ImageArea
and for drawing I simply store two mouse locations.
Here are the class level variable I use:
PointF mDown = Point.Empty;
PointF mLast = Point.Empty;
float zoom = 1f;
RectangleF ImgArea = RectangleF.Empty;
Note that I use floats
for all, since we will need to do some dividing..
First we'll calculate the zoom
and the ImageArea
:
void GetImageScaleData(PictureBox pbox)
{
SizeF sp = pbox.ClientSize;
SizeF si = pbox.Image.Size;
float rp = 1f * sp.Width / sp.Height; // calculate the ratios of
float ri = 1f * si.Width / si.Height; // pbox and image
if (rp > ri)
{
zoom = sp.Height / si.Height;
float width = si.Width * zoom;
float left = (sp.Width - width) / 2;
ImgArea = new RectangleF(left, 0, width, sp.Height);
}
else
{
zoom = sp.Width / si.Width;
float height = si.Height * zoom;
float top = (sp.Height - height) / 2;
ImgArea = new RectangleF(0, top, sp.Width, height);
}
}
This routine should be called each time a new Image
is loaded and also upon any Resizing of the PictureBox
:
private void pictureBox1_Resize(object sender, EventArgs e)
{
GetImageScaleData(pictureBox1);
}
Now ne need store the mouse locations. Since they must be reusable after a resize we need to tranfsorm them to image coordinates. This routine can do that and also back again:
PointF scalePoint(PointF pt, bool scale)
{
return scale ? new PointF( (pt.X - ImgArea.X) / zoom, (pt.Y - ImgArea.Y) / zoom)
: new PointF( pt.X * zoom + ImgArea.X, pt.Y * zoom + ImgArea.Y);
}
Finally we can code the Paint
event
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
using (Pen pen = new Pen(Color.Fuchsia, 2.5f) { DashStyle = DashStyle.Dot})
e.Graphics.DrawRectangle(pen, Rectangle.Round(ImgArea));
e.Graphics.SetClip(ImgArea);
e.Graphics.DrawLine(Pens.Red, scalePoint(mDown, false), scalePoint(mLast, false));
}
.. and the mouse events:
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
mDown = scalePoint(e.Location, true);
}
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
mLast = scalePoint(e.Location, true);
pictureBox1.Invalidate();
}
}
For more complex drawing you would store the coordinates in List<PointF>
and transform them back, pretty much like above..:
List<PointF> points = new List<PointF>();
and then:
e.Graphics.DrawCurve(Pens.Orange, points.Select(x => scalePoint(x, false)).ToArray());
How to Draw on Zoomable Image in C# windows Forms
Here is a PictureBox
subclass that supports the ability to apply zooming not only to the Image
but also to graphics you draw onto its surface.
It includes a SetZoom
function to zoom in by scaling both itself and a Matrix.
It also has a ScalePoint
function you can use to calculate the unscaled coordinates from the pixel coordinates you receive in the mouse events.
The idea is to use a Transformation Matrix
to scale any pixels the Graphics
object will draw in the Paint
event.
I include a little code for the form for testing.
public partial class ScaledPictureBox : PictureBox
{
public Matrix ScaleM { get; set; }
float Zoom { get; set; }
Size ImgSize { get; set; }
public ScaledPictureBox()
{
InitializeComponent();
ScaleM = new Matrix();
SizeMode = PictureBoxSizeMode.Zoom;
}
public void InitImage()
{
if (Image != null)
{
ImgSize = Image.Size;
Size = ImgSize;
SetZoom(100);
}
}
public void SetZoom(float zoomfactor)
{
if (zoomfactor <= 0) throw new Exception("Zoom must be positive");
float oldZoom = Zoom;
Zoom = zoomfactor / 100f;
ScaleM.Reset();
ScaleM.Scale(Zoom , Zoom );
if (ImgSize != Size.Empty) Size = new Size((int)(ImgSize.Width * Zoom),
(int)(ImgSize.Height * Zoom));
}
public PointF ScalePoint(PointF pt)
{ return new PointF(pt.X / Zoom , pt.Y / Zoom ); }
}
Here is the code in the Form that does the testing:
public List<PointF> somePoints = new List<PointF>();
private void scaledPictureBox1_MouseClick(object sender, MouseEventArgs e)
{
somePoints.Add(scaledPictureBox1.ScalePoint(e.Location) );
scaledPictureBox1.Invalidate();
}
private void scaledPictureBox1_Paint(object sender, PaintEventArgs e)
{
// here we apply the scaling matrix to the graphics object:
e.Graphics.MultiplyTransform(scaledPictureBox1.ScaleM);
using (Pen pen = new Pen(Color.Red, 10f))
{
PointF center = new PointF(scaledPictureBox1.Width / 2f,
scaledPictureBox1.Height / 2f);
center = scaledPictureBox1.ScalePoint(center);
foreach (PointF pt in somePoints)
{
DrawPoint(e.Graphics, pt, pen);
e.Graphics.DrawLine(Pens.Yellow, center, pt);
}
}
}
public void DrawPoint(Graphics G, PointF pt, Pen pen)
{
using (SolidBrush brush = new SolidBrush(pen.Color))
{
float pw = pen.Width;
float pr = pw / 2f;
G.FillEllipse(brush, new RectangleF(pt.X - pr, pt.Y - pr, pw, pw));
}
}
Here are the results after drawing a few points showing the same points in four different zoom settings; the ScaledPictureBox
is obviously placed in an AutoScroll-Panel
. The lines show how to use the regular drawing commands..
QPainter: drawing only the visible area of a zoomed-in image
The general idea is to intersect the image rectangle with the paint area rectangle, that is the item rectangle ({0, 0, width(), height()}
). Such intersection has to be done in a chosen coordinate system, and the rectangle has to be propagated to the other coordinate system. Let's do the intersection in the target coordinate system:
// **private
private:
QImage mImage;
QPointF mOffset;
double mZoom = 1.0;
double mRenderTime = 0.;
bool mRectDraw = true;
QRectF mSourceRect;
QRectF mTargetRect;
static void moveBy(QRectF &r, const QPointF &o) {
r = {r.x() + o.x(), r.y() + o.y(), r.width(), r.height()};
}
static void scaleBy(QRectF &r, qreal s) {
r = {r.x() * s, r.y() * s, r.width() * s, r.height() * s};
}
void recalculate() {
const auto oldTargetRect = mTargetRect;
const auto oldSourceRect = mSourceRect;
mTargetRect = {{}, mImage.size()};
moveBy(mTargetRect, -mOffset);
scaleBy(mTargetRect, mZoom);
mTargetRect = mTargetRect.intersected({{}, size()});
Now we transform that rectangle back into the source (image) coordinate system:
mSourceRect = mTargetRect;
scaleBy(mSourceRect, 1.0/mZoom);
moveBy(mSourceRect, mOffset);
if (mTargetRect != oldTargetRect)
emit targetRectChanged(mTargetRect);
if (mSourceRect != oldSourceRect)
emit sourceRectChanged(mSourceRect);
update();
}
Then one has to choose how to scroll - generally the scroll range is simply anywhere within the source image's rectangle (i.e. mImage.rect()
, recalling that it is {0, 0, mImage.width(), mImage.height()}
), thus the x/y scroll sliders go between 0 and the width/height of the image, respectively.
The painting could also be implemented by painting the entire image, but unfortunately the paint engine backing the painter doesn't know how to handle clipping - so even if we set clipping right before drawImage
, it won't do anything: the painter we have to work with ignores clipping. And thus, at high zoom values, the painting with mRectDraw = false
becomes inefficient. This is a deficiency of the paint engine and it definitely could be fixed up in Qt proper.
// **paint
void paint(QPainter *p) override {
QElapsedTimer timer;
timer.start();
if (mRectDraw) {
p->drawImage(mTargetRect, mImage, mSourceRect);
} else {
p->scale(mZoom, mZoom);
p->translate(-mOffset);
p->drawImage(0, 0, mImage);
}
mRenderTime = timer.nsecsElapsed() * 1E-9;
emit renderTimeChanged(mRenderTime);
}
The remainder of the example follows. The meaning of the zoom spinbox is an exponent on sqrt(2)
, i.e. value=0 -> zoom=1
, value=-2 -> zoom=0.5
, `value=4 -> zoom=2', etc. The canvas supports positive non-zero zoom values, i.e. also values below 1.
// https://github.com/KubaO/stackoverflown/tree/master/questions/qml-zoom-imagecanvas-51455895
#include <QtQuick>
#include <limits>
class ImageCanvas : public QQuickPaintedItem {
Q_OBJECT
Q_PROPERTY(QImage image READ image WRITE setImage NOTIFY imageChanged)
Q_PROPERTY(QRectF imageRect READ imageRect NOTIFY imageRectChanged)
Q_PROPERTY(QPointF offset READ offset WRITE setOffset NOTIFY offsetChanged)
Q_PROPERTY(double zoom READ zoom WRITE setZoom NOTIFY zoomChanged)
Q_PROPERTY(double renderTime READ renderTime NOTIFY renderTimeChanged)
Q_PROPERTY(bool rectDraw READ rectDraw WRITE setRectDraw NOTIFY rectDrawChanged)
Q_PROPERTY(QRectF sourceRect READ sourceRect NOTIFY sourceRectChanged)
Q_PROPERTY(QRectF targetRect READ targetRect NOTIFY targetRectChanged)
public:
ImageCanvas(QQuickItem *parent = {}) : QQuickPaintedItem(parent) {}
QImage image() const { return mImage; }
QRectF imageRect() const { return mImage.rect(); }
void setImage(const QImage &image) {
if (mImage != image) {
auto const oldRect = mImage.rect();
mImage = image;
recalculate();
emit imageChanged(mImage);
if (mImage.rect() != oldRect)
emit imageRectChanged(mImage.rect());
}
}
Q_SIGNAL void imageChanged(const QImage &);
Q_SIGNAL void imageRectChanged(const QRectF &);
QPointF offset() const { return mOffset; }
void setOffset(const QPointF &offset) {
mOffset = offset;
recalculate();
emit offsetChanged(mOffset);
}
Q_SIGNAL void offsetChanged(const QPointF &);
double zoom() const { return mZoom; }
void setZoom(double zoom) {
if (zoom != mZoom) {
mZoom = zoom ? zoom : std::numeric_limits<float>::min();
recalculate();
emit zoomChanged(mZoom);
}
}
Q_SIGNAL void zoomChanged(double);
// **paint
double renderTime() const { return mRenderTime; }
Q_SIGNAL void renderTimeChanged(double);
bool rectDraw() const { return mRectDraw; }
void setRectDraw(bool r) {
if (r != mRectDraw) {
mRectDraw = r;
recalculate();
emit rectDrawChanged(mRectDraw);
}
}
Q_SIGNAL void rectDrawChanged(bool);
QRectF sourceRect() const { return mSourceRect; }
QRectF targetRect() const { return mTargetRect; }
Q_SIGNAL void sourceRectChanged(const QRectF &);
Q_SIGNAL void targetRectChanged(const QRectF &);
protected:
void geometryChanged(const QRectF &, const QRectF &) override {
recalculate();
}
// **private
};
QImage sampleImage() {
QImage image(500, 500, QImage::Format_ARGB32_Premultiplied);
QPainter painter(&image);
for (int y = 0; y < image.height(); y += 50)
for (int x = 0; x < image.width(); x += 50) {
const QColor colour((x / 500.0) * 255, (y / 500.0) * 255, 0);
painter.fillRect(x, y, 50, 50, colour);
}
return image;
}
int main(int argc, char *argv[])
{
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<ImageCanvas>("App", 1, 0, "ImageCanvas");
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("sampleImage", sampleImage());
engine.load(QUrl("qrc:/main.qml"));
return app.exec();
}
#include "main.moc"
And the qml:
import QtQuick 2.10
import QtQuick.Controls 2.3
import App 1.0
ApplicationWindow {
id: window
width: 600
height: 600
visible: true
title: "T=" + (canvas.renderTime*1E3).toFixed(1) + "ms t=" + canvas.targetRect + " s=" + canvas.sourceRect
ImageCanvas {
id: canvas
image: sampleImage
anchors.fill: parent
offset: Qt.point(xOffsetSlider.value, yOffsetSlider.value)
zoom: Math.pow(Math.SQRT2, zoomSpinBox.value)
rectDraw: rectDrawCheckBox.checked
}
SpinBox {
id: zoomSpinBox
anchors.bottom: xOffsetSlider.top
from: -10
to: 20
}
CheckBox {
id: rectDrawCheckBox
anchors.left: zoomSpinBox.right
anchors.bottom: xOffsetSlider.top
text: "rectDraw"
checked: true
}
Slider {
id: xOffsetSlider
anchors.bottom: parent.bottom
width: parent.width - height
from: 0
to: canvas.imageRect.width
ToolTip {
id: xOffsetToolTip
parent: xOffsetSlider.handle
visible: true
text: xOffsetSlider.value.toFixed(1)
Binding {
target: xOffsetToolTip
property: "visible"
value: !yOffsetToolTip.visible
}
}
}
Slider {
id: yOffsetSlider
anchors.right: parent.right
height: parent.height - width
orientation: Qt.Vertical
from: canvas.imageRect.height
to: 0
ToolTip {
id: yOffsetToolTip
parent: yOffsetSlider.handle
text: yOffsetSlider.value.toFixed(1)
Binding {
target: yOffsetToolTip
property: "visible"
value: !xOffsetToolTip.visible
}
}
}
}
Related Topics
Removing Hidden Characters from Within Strings
How to Write a Comment to an Xml File When Using the Xmlserializer
Efficient Way to Round Double Precision Numbers to a Lower Precision Given in Number of Bits
Update Requires a Valid Updatecommand When Passed Datarow Collection with Modified Rows
Panel for Drawing Graphics and Scrolling
.Net: Unable to Cast Object to Interface It Implements
Wpf Dispatcher {"The Calling Thread Cannot Access This Object Because a Different Thread Owns It."}
Draw Multiple Freehand Polyline or Curve Drawing - Adding Undo Feature
Dynamically Create a Generic Type for Template
How to Find the Position of a Cursor in a Text Box? C#
The Operation Cannot Be Completed Because the Dbcontext Has Been Disposed Using MVC 4
Missing Providername When Debugging Azurefunction as Well as Deploying Azure Function