I have a problem in my app. I draw lines and points with the help of mouse and I need to detect wether I intersect the line or get the point. And with points it works well, but with lines it doesn't work. I add lines to the AnchorPane. Then I spotted, when cursor is situated inside of 3 lines (Triange) - it's always intersect. Here is my example:
ArrayList<Line> lines = new ArrayList<Line>();
Line l1 = new Line(10, 150, 50, 10);
Line l2 = new Line(10, 150, 100, 150);
Line l3 = new Line(100, 150, 50, 10);
lines.add(l1);
lines.add(l2);
lines.add(l3);
for (int i=0; i<lines.size();++i) {
if (lines.get(i).intersects(48, 48, 4, 4)) {
System.out.println("found a bug!" + " line #"+i);
}
}
If someone knew the answer - it would be great!
I'll imagine that your question is: how to pick a line by clicking anywhere within two pixels of the line?
In the sample output below, the user has just clicked very close to the right line of the triangle, causing that line to be highlighted.
The code below works by testing each shape in the pane and seeing if the shape intersects a rectangular box 2 pixels on either side of a mouse press location. The solution is very similar to that used in solving: Checking Collision of Shapes with JavaFX.
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.*;
import javafx.stage.Stage;
public class LinePicker extends Application {
public static void main(String[] args) { Application.launch(LinePicker.class); }
#Override public void start(final Stage stage) throws Exception {
final Pane pane = new Pane();
pane.getChildren().setAll(
new Line( 10, 150, 50, 10),
new Line( 10, 150, 100, 150),
new Line(100, 150, 50, 10)
);
pane.setPrefSize(
200, 200
);
Scene scene = new Scene(addPickHandler(pane));
stage.setScene(scene);
stage.show();
}
private Pane addPickHandler(final Pane pane) {
pane.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent event) {
final Rectangle hotspot = new Rectangle(
event.getX() - 2,
event.getY() - 2,
4,
4
);
for (Node child : pane.getChildren()) {
if (child instanceof Shape) {
final Shape shape = (Shape) child;
Shape intersect = Shape.intersect(shape, hotspot);
if (intersect.getBoundsInLocal().getWidth() != -1) {
shape.setStyle("-fx-stroke: red;");
} else {
shape.setStyle("-fx-stroke: black;");
}
}
}
}
});
return pane;
}
}
Comment
It is a little confusing to me why shved90 was unable to get the above solution to function for him in his application.
Shev comments that "In Line, as I understand, it checks all the bounds and if the line would be with some angle its bound would be like rectangle and as a result I check whether I spotted on it (not only on line and its nearest points). But I needed geometry operation."
However the solution presented here is a geometry based solution in that the click hotspot must intersect the actual line and not the rectangular bounds encompassing the line. This is because the Shape.intersect method used in the solution works on the intersection of the actual shapes involved and not the bounds of the shapes involved.
Note however that in the shved90's original question, he uses the Node intersects method rather than the Shape.intersect method. The documentation of Node intersects states "The default behavior of this function is simply to check if the given coordinates intersect with the local bounds.", which, from his comment, is obviously not what shved90 wanted.
Shev does note that "I've found Line2d class in javaFx, as like in Java2D, I convert to this line and use it's intersection". I would not recommend using the Line2D class bundled with JavaFX as it is a private com.sun api that is not guaranteed to be present or have a binary backward compatible API in future JavaFX releases.
Related
I wanted to have an online monitoring system that could tell where the shape is currently, but am getting very weird coordinates of the item, also the dimensions of it get higher by 1 each time I create new one and drag it.
Initial position (map size is 751 by 751, checked by outputting to qDebug(), scene bound to yellow space) :
Dragging it to the left top corner.
As you can see in the beginning it was on (200;200), but after dragging it is on (-201;-196). After deleting it and creating new shape on the same position with the same properties, new shape can't be seen because it is outside of the map, which suggests that edits don't show correct data.
Here is the code of updating the edits:
void CallableGraphicsRectItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
QGraphicsRectItem::mouseReleaseEvent(event);
ptr->updateEdits(this);
}
Here is what I managed to cut down into updateEdits():
void MainWindow::updateEdits(QAbstractGraphicsShapeItem* item)
{
//stuff not related to scene
auto posReal = item->scenePos();
auto pos = posReal.toPoint();
//create QString from coordinates
QString coordinate;
coordinate.setNum(pos.x());
ui->leftXEdit->setText(coordinate);
coordinate.setNum(pos.y());
ui->upperYEdit->setText(coordinate);
//get width and height for rect, radius for circle
auto boundingRectReal = item->sceneBoundingRect();
auto boundingRect = boundingRectReal.toRect();
ui->widthEdit->setText(QString::number(boundingRect.width()));
//disables height edit for circles, not really relevant
if (!items[currentShapeIndex].isRect)
{
ui->heightEdit->setDisabled(true);
}
else
{
ui->heightEdit->setDisabled(false);
ui->heightEdit->setText(QString::number(boundingRect.height()));
}
}
Here is how I anchor the QGraphicsScene to the left top corner of the yellow area:
scene->setSceneRect(0, 0, mapSize.width() - 20, mapSize.height() - 20);
ui->graphicsView->setScene(scene);
How can I report the right data to the edits?
You're better off overriding the itemChange method and using the ItemPositionHasChanged notification. You have to set the ItemSendsGeometryChanges flag on the item so that it receives these notifications.
I'm not sure that your item's final position has been set when you're still in the mouseReleaseEvent method. Tracking it in itemChange will ensure that the data is valid, and this kind of thing is what it's for.
Also, note that "pos" is in the item's parent coordinates, and "boundingRect" is in the item's coordinate space. You should use "scenePos" and "sceneBoundingRect" if you want to be sure you're using scene coordinates. If the item doesn't have a parent, then "pos" and "scenePos" will return the same values, but "boundingRect" and "sceneBoundingRect" will generally differ.
I am just learning Qt. I want to show line number of QPlainTextEdit. I found this link
and it worked. But now I want the editor displays the line numbers in an area to the RIGHTof the area for editing. I have been searching google very much, but I can't solve. How to solve?
In addition to GPPK's answer, you also need to change the viewport margins:
void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
{
setViewportMargins(0, 0, lineNumberAreaWidth(), 0);
}
GPPK's code assigns the correct drawing rectangle to the sub-widget, my code makes sure, that the scrollview does not paint into that area.
In your link it shows you how it draws the line number area on the left:
void CodeEditor::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect();
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}
In order to draw the line numbers from the right you will (this is untested) do something like this:
void CodeEditor::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect();
lineNumberArea->setGeometry(QRect(cr.right() - lineNumberArea->width() , cr.top(), lineNumberAreaWidth(), cr.height()));
}
I am making a small game in C++11 with Qt. However, I am having some issues with scaling.
The background of my map is an image. Each pixel of that image represents a tile, on which a protagonist can walk and enemies/healthpacks can be.
To set the size of a tile, I calculat the maximum amount like so (where imageRows & imageCols is amount of pixels on x- and y-axis of the background image):
QRect rec = QApplication::desktop()->screenGeometry();
int maxRows = rec.height() / imageRows;
int maxCols = rec.width() / imageCols;
if(maxRows < maxCols){
pixSize = maxRows;
} else{
pixSize = maxCols;
}
Now that I have the size of a tile, I add the background-image to the scene (in GameScene ctor, extends from QGraphicsScene):
auto background = new QGraphicsPixmapItem();
background->setPixmap(QPixmap(":/images/map.png").scaledToWidth(imageCols * pixSize));
this->addItem(background);
Then for adding enemies (they extend from a QGraphicsPixMapItem):
Enemy *enemy = new Enemy();
enemy->setPixmap(QPixmap(":/images/enemy.png").scaledToWidth(pixSize));
scene->addItem(enemy);
This all works fine, except that on large maps images get scaled once (to a height of lets say 2 pixels), and when zooming in on that item it does not get more clear, but stays a big pixel. Here is an example: the left one is on a small map where pixSize is pretty big, the second one has a pixSize of pretty small.
So how should I solve this? In general having a pixSize based on the screen resolution is not really useful, since the QGrapicsScene is resized to fit the QGraphicsView it is in, so in the end the view still determines how big the pixels show on the screen.
MyGraphicsView w;
w.setScene(gameScene);
w.fitInView(gameScene->sceneRect(), Qt::KeepAspectRatio);
I think you might want to look at the chip example from Qt (link to Qt5 but also works for Qt4).
The thing that might help you is in the chip.cpp file:
in the paint method:
const qreal lod = option->levelOfDetailFromTransform(painter->worldTransform());
where painter is simply a QPainter and option is of type QStyleOptionGraphicsItem. This quantity gives you back a measure of the current zoom level of your QGraphicsView and thus as in the example you can adjust what is being drawn at which level, e.g.
if (lod < 0.2) {
if (lod < 0.125) {
painter->fillRect(QRectF(0, 0, 110, 70), fillColor);
return;
}
QBrush b = painter->brush();
painter->setBrush(fillColor);
painter->drawRect(13, 13, 97, 57);
painter->setBrush(b);
return;
}
[...]
if (lod >= 2) {
QFont font("Times", 10);
font.setStyleStrategy(QFont::ForceOutline);
painter->setFont(font);
painter->save();
painter->scale(0.1, 0.1);
painter->drawText(170, 180, QString("Model: VSC-2000 (Very Small Chip) at %1x%2").arg(x).arg(y));
painter->drawText(170, 200, QString("Serial number: DLWR-WEER-123L-ZZ33-SDSJ"));
painter->drawText(170, 220, QString("Manufacturer: Chip Manufacturer"));
painter->restore();
}
Does this help?
This is just an experiment I'm doing, but suppose that we have three rectangles that are drawn:
void thisDialog::paintEvent(QPaintEvent *ev)
{
rectangle = new QRect(40, 80, 40, 90);
painter = new QPainter(this);
brush = new QBrush(QColor(QColor::green);
painter->fillRect(...);
painter->drawRect(...);
rectangle2 = new QRect(140, 80, 40, 90);
// and draw
rectangle3 = new QRect(240, 80, 40, 90);
// and draw...
}
And now I want to somehow shuffle the rectangles drawn. My solution is to use std::random_shuffle. The plan is we push all the rectangles into a vector thereof:
vector<QRect*> vecRect;
vecRect.push_back(rectangle);
// push back other rectangles
std::random_shuffle(vecRect.begin(), vecRect.end());
At this point, the order of the rectangles have been shuffled. The only problem is that although they're shuffled I have to somehow reflect that in their positions and also update what's shown on-screen for the changes to be apparent. So, I propose creating a function called shuffle. I want only the x-values to change, so the y-values and the spacings of the rectangles should be left alone.
void shuffle()
{
std::random_shuffle(vecRect.begin(), vecRect.end());
for (auto it : vecRect)
{
// do something to the x-values in rectangle; e.g.
it->setX(/*set something...*/);
}
}
To shuffle the rectangles along the x-axis and maintain the y-axis, and maintain the spacing in between I'd have to get a random number and add the spacing and ensure that it is within the window boundaries say 800x600.
Since I am new to Qt I have to know how Qt handles primitive positions, and so I guess that setX() from the docs would modify the x-position of the rectangle. My other enquiry is: how do we update the positions so they're reflected on what's drawn on-screen? Whenever I update positions it doesn't seem to take effect. I've tried update() but that doesn't seem to work.
Btw, I call the foresaid function through a key press, so if the key 'S' is pressed then the function is called.
TLDR; How can I shuffle x-positions in these rectangles and update their positions rendered?
I have been toying with this piece of code:
QGraphicsLineItem * anotherLine = this->addLine(50,50, 100, 100);
qDebug() << anotherLine->scenePos();
QGraphicsLineItem * anotherLine2 = this->addLine(80,10, 300, 300);
qDebug() << anotherLine2->scenePos();
Where the this pointer refers to a QGraphicsScene. In both cases, I get QPointF(0,0) for both output.From reading the document, I thought scenePos() is supposed to return the position of the line within the scene, not where it is within its local coordinate system. What am I doing wrong?
After reading the QT 4.5 documentation carefully on addLine, I realize what I have been doing wrong. According to the doc:
Note that the item's geometry is
provided in item coordinates, and its
position is initialized to (0, 0)
So if I specify addLine(50,50, 100, 100), I am actually modifying its local item coordinate. The assumption I made that it will be treated as a scene coordinate is wrong or unfounded. What I should be doing is this
// Create a line of length 100
QGraphicsItem * anotherLine = addLine(0,0, 100, 100);
// move it to where I want it to be within the scene
anotherLine->setPos(50,50);
So if I am adding a line by drawing within the scene, I need to reset its centre to (0,0) then use setPos() to move it to where I want it to be in the scene.
Hope this helps anyone who stumble upon the same problem.