Failed to Update .Mdf Database Because the Database Is Read-Only (Windows Application)

How to draw on a zoomed image?

You need to address two issues:

  • Clip the Graphics area to the actual Image instead of the whole PictureBox.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();
}
}

enter image description here

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..

enter image description hereenter image description hereenter image description hereenter image description here

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



Leave a reply



Submit