I am currently developing a small programm in Qt. To show a plot you can use qwt or qcustomplot or the qpainterevent or QChart. But I am interessted in a solution for a dynamic plot which is writen with the QGraphicsView.
My preferences
-width of my chart should be constant
-realtime plotting
-the first sample should be deleted or overwriten if the end of the chart is reached, so it is a dynamic and fluent chart
My example beneath is able to be dynamic and fluent... but just for the number, which is in my if clause. I do not get why.
The idea is to delete the first lineitem, so I have constantly 99 items. If I delete a item I want to give the next item the position from the item before.
So
x=99 will be x=98 ......x=1 will be x=0;
Do I have a mistake in my idea?
I also have had several ideas, but this is probably the best.
Thanks in advance
Konrad
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
scene = new QGraphicsScene(this);
ui->graphicsView->setScene(scene);
vectorPoint = new QVector<QPoint>;
line = new QVector<QGraphicsLineItem*>;
yDatai = 0;
xDatai = 0;
Grenzenlaufvariable = 0;
timer = new QTimer(this);
timer->start(10);
connect (timer, SIGNAL(timeout()),this,SLOT(newData()));
connect(this,SIGNAL(newPaint()),this,SLOT(paint()));
}
MainWindow::~MainWindow()
{
delete ui;
delete scene;
delete vectorPoint;
delete line;
}
void MainWindow::newData()
{
if (yDatai == 100 || yDatai == -100)
{
Grenzenlaufvariable++;
}
if (Grenzenlaufvariable%2==0)
{
yDatai+=1;
}
else
{
yDatai-=1;
}
xDatai++;
point = {xDatai,yDatai};
vectorPoint->append(point);
if(vectorPoint->size()>1)
{
item = scene->addLine(QLineF(vectorPoint->at(ix-1),vectorPoint->at(ix)));
line->append(item);
}
ix++;
emit newPaint();
}
void MainWindow::paint()
{
if(line->size()==99)
{
scene->removeItem(line->at(0));
line->removeAt(0);
qDebug()<<line->size();
for (int ip= 0;ip <line->size();ip++)
{
oldx = line->at(ip)->x();
line->at(ip)->setX(oldx-1);
qDebug()<<ip;
}
}
}
So far, thats the best answer, pay attention that if you use 100Hz as samplerate, my performance is just stable with 50 samplesInView.
You can decrease the samplerate and increase the samplesInView to have more values in the plot.
Important:
xDatashort is a QVector<double> which includes all the x-values
yDatashort is a QVector<double> which includes all the y-values
both are filled with values in the programm class, this class is emitting the signal to the connection which start the slot drawGraph().
You can also just use an QVector<QPoint> which makes it more easy to handle, but its not what I want in my case.
lineVector is a QVector<QGraphicsLineItem> which inclues all the Lines from the view
xScale is used to extend the plot, yScale as well.
width is the width of the Coordinationsystem
xAxisMark is the pixeldistance between to distance marks
marksVector is a QVector<double> which includes the distance marks of the x Axis, which should be dynamic
iCurrentVectorPoint is a runtimevariable, which helps me to add the lines.
!!This code is good to use for realtime plotting, but it has not the best performance, so if anybody is having ideas to unleash potential, feel free to achieve the best answer :) !!
For further questions just comment and I will try to help you to get a nice handmade plot on your device.
void Plot::drawGraph()
{
if(programm->xDatashort.size()>1)
{
if(lineVector->size()==programm->samplesInView)
{
for (int ip =0;ip<programm->samplesInView;ip++)
{
lineVector->at(ip)->setLine((ip)*xScale,(programm->yDatashort.at(ip))*yScale*(-1),(ip+1)*xScale,(programm->yDatashort.at(ip+1))*yScale*(-1));
}
for (int iy=1 ; iy<(width/xAxisMarks)+1 ; iy++)
{
int oldx = marksVector->at(iy)->x();
oldx-=1;
if(oldx%xAxisMarks==0 || oldx==0)
{
marksVector->at(iy)->setX(oldx+xAxisMarks);
}
else
{
marksVector->at(iy)->setX(oldx);
}
}
}
else
{
item = scene->addLine(QLineF(programm->xDatashort.at(iCurrentVectorPoint-1)*xScale, programm->yDatashort.at(iCurrentVectorPoint-1)*yScale*(-1), programm->xDatashort.at(iCurrentVectorPoint)*xScale, programm->yDatashort.at(iCurrentVectorPoint)*yScale*(-1)));
lineVector->append(item);
}
}
iCurrentVectorPoint++;
}
Update:
Code stable for more than 50 min with 800 samples in view, with 100 Hz samplerate and 20 Hz framerate. Using a Thread for the simulatordata. Feel free to ask me nearly everything about this topic, I worked through it for nearly 2 months :D
void MainWindow::drawChart()
{
//To check the framerate I implemented a framecounter
framerunner++;
ui->Framerate->setText(QString::number(int(framerunner/double(DurationTimer->elapsed()/1000))));
//Using to stay focused on the scene, not neccesary if you define the x values from [startview-endview]
QRect a;
if(Samplevector.size()!=0)
{
a.setRect(Samplevector.at(Samplevector.size()-1).getX()-850,0,900,200);
qDebug()<<Samplevector.at(Samplevector.size()-1).getX();
ui->LinegraphView->setSceneRect(a);
}
//delete everything in the scene and redraw it again
scene->clear();
if(Samplevector.size()>1)
{
for(int i=1;i<Samplevector.size();i++)
scene->addLine(QLineF(Samplevector.at(i-1).getX(),Samplevector.at(i-1).getY(),Samplevector.at(i).getX(),Samplevector.at(i).getY()));
}
}
void MainWindow::start()
{
framerate->start(50);
DurationTimer->start();
hegsimulator->moveToThread(thread);
thread->start();
qDebug()<<"Request "<<this->QObject::thread()->currentThreadId();
}
void MainWindow::stop()
{
framerate->stop();
hegsimulator->stopDevice();
}
void MainWindow::prepareGraph()
{
samplerunner++;
ui->Samplerate->setText(QString::number(int(samplerunner/double(DurationTimer->elapsed()/1000))));
Samplevector.append(hegsimulator->getSample());
if(Samplevector.size()>800)
{
//graphlinevector.first()->hide();
//scene->removeItem(graphlinevector.first());
// graphlinevector.removeFirst();
Samplevector.removeFirst();
}
// if(Samplevector.size()>1)
// {
// item = scene->addLine(QLineF(Samplevector.at(Samplevector.size()-2).getX(),Samplevector.at(Samplevector.size()-2).getY(),Samplevector.at(Samplevector.size()-1).getX(),Samplevector.at(Samplevector.size()-1).getY()));
// graphlinevector.append(item);
// }
}
CAUTION!!
These Solutions are not the fastest ones. Use QPainterPath instead of QLineF, it is like you take a pen, draw a line, put the pen away, and this 1000 times.
Better to safe all QPoints in a QPainterPath and take the pen once to draw. That increases the performance to Realtime Plotting with a 4 min trend and more without problems.
Related
I'm in the process of creating a QT application which is using multiple (14) qcheckboxes. I need to have a limit (preferably set as a variable that i can change) to the number of checkboxes that can be checked at the same time, is there any way to achieve this cleanly ? Thanks for your time.
There is no simple way of doing this, you have to write your code to do it.
I suppose you have the checkboxes in some parent widget class. So I would create a slot which looks like this.
void SomeParentWidget::onCheckBoxToggled(bool value)
{
// when we unchecked the checkbox,
// we do not need to count the number of checked ones
if (!value)
return;
int total = 0;
int limit = 15; // your "magic" number of maximum checked checkboxes
for (auto chb : allCheckBoxes()) // allCheckBoxes() is some method which returns all the checkboxes in consideration
{
if (chb->isChecked())
{
++total;
if (total > limit)
{
// too many checkboxes checked! uncheck the sender checkbox
// Note: you may want to add some nullptr checks or asserts to the following line for better robustness of your code.
qobject_cast<QCheckBox*>(sender())->setChecked(false);
return;
}
}
}
}
And when creating each of your checkboxes inside some parent widget, connect this slot to their signal:
auto chb = new QCheckBox();
connect(chb, &QCheckBox::toggled, this, &SomeParentWidget::onCheckBoxToggled);
Implementation of allCheckBoxes() is up to you, I do not know how you can retrieve the collection of all your check boxes. Depends on your design.
I found another, even simpler solution. Use this slot.
void SomeParentWidget::onCheckBoxToggled(bool value)
{
static int totalChecked = 0; // static! the value is remembered for next invocation
totalChecked += value ? 1 : -1;
Q_ASSERT(totalChecked >= 0);
int maxChecked = 15; // any number you like
if (value && totalChecked > maxChecked)
{
qobject_cast<QCheckBox*>(sender())->setChecked(false);
}
}
... and connect it to checkboxes' toggled() signal. Note that in order to work correctly, all check boxes must be unchecked at the time when you make the signal-slot connection because this function starts counting from zero (0 is the initial value of the static variable).
You can store all your checkboxes in a map (either in an std::map, an std::unordered_map or an QMap). Your keys will be your checkboxes, and your values will be their states, so something like this:
std::unordered_map<QCheckBox*, bool> m_checkBoxStates;
Here's what your connected to your toggled signal of all your checkboxes look like (keep in mind that all the signals will be connected to the same slot):
void MainWindow::onToggled(bool checked) {
QCheckBox* checkBox = sender(); //the checkbox that has been toggled
m_checkBoxStates[checkBox] = checked;
if (!checked) {
return;
}
const int count = std::count_if(m_checkBoxStates.begin(), m_checkBoxStates.end(),
[](const auto pair) {
return pair.second == true;
});
if (count > maxCount) {
checkBox->setChecked(false);
}
}
Is it possible to pin a tab with Qt?
I want a tab to always stay in place (index 0) while still able to move other tabs.
So far I tried to listen to QTabBar::tabMoved and revert the move but that's too late. I don't want it even to attempt to move.
Worst case for me would be to be forced to change the mouse handling. Let me know please if there is an other way.
I have never found a nice way to do that. But, I used the fact that you can store raw data in the QTabBar to pin the tabs and undo a move if it was a pinned tab. It's not perfect and I still have some ugly behavior, but I didn't want to use mouse events, neither.
First, create a struct to store the current state of a tab:
struct PinnedTab
{
bool isPinned;
int currentIndex;
};
Q_DECLARE_METATYPE(PinnedTab); // For QVariant
Then, create a custom QTabBar to handle the move and use QTabWidget to replace the current tab bar (you have to do that before inserting the tabs):
class Bar: public QTabBar
{
public:
void pin(int const index)
{
PinnedTab info;
info.isPinned = true;
info.currentIndex = index; // TODO: move the tab to the left and do not use current index
setTabData(index, QVariant::fromValue(info));
}
Bar(QWidget* parent=nullptr): QTabBar(parent)
{}
virtual void tabLayoutChange() override
{
for (int i = 0; i != count(); ++i) // Check if a pinned tab has moved
{
if (tabData(i).isValid())
{
PinnedTab const info = tabData(i).value<PinnedTab>();
if (info.isPinned == true && i != info.currentIndex) {
rollbackLayout();
return;
}
}
}
for (int i = 0; i != count(); ++i)
{
if (tabData(i).isValid())
{
PinnedTab info = tabData(i).value<PinnedTab>();
info.currentIndex = i;
setTabData(i, QVariant::fromValue(info));
}
else
{
PinnedTab info;
info.isPinned = false;
info.currentIndex = i;
setTabData(i, QVariant::fromValue(info));
}
}
}
void rollbackLayout() {
for (int i = 0; i != count(); ++i)
{
if (tabData(i).isValid())
{
PinnedTab const info = tabData(i).value<PinnedTab>();
if (i != info.currentIndex) {
moveTab(i, info.currentIndex);
}
}
}
}
};
tabLayoutChange is called when the layout has changed. So, it will be called when you move a tab.
the rollbackLayout method is used to move each tab to the last position stored in the tab data.
Call pin to pin a tab with the given index.
I simplified my code for more clarity and you may have to redefine some behavior (for now, if you pin a tab, it will keep its current position and it will not handle the insert/remove tabs).
I'm using gifs in my app. I'd like to only show my gif once and then make it disappear. But it keeps looping and showing again and again.
Here's what I've tried:
movie = new QMovie(":/in_game/src/countdown.gif");
processLabel = new QLabel();
this->addWidget(processLabel);
processLabel->setStyleSheet("background-color: rgba(0,0,0,0%)");
processLabel->setGeometry(280,250,128,128);
processLabel->setMovie(movie);
int i=0;
if(i<1)
{
movie->start();
i++;
}
else
{
movie->stop();
processLabel->setEnabled(false);
}
Of course in my .h I've created a QMovie and a QLabel... Any ideas on how to only display once ?
You have QMovie::frameCount() method and signal QMovie::frameChanged(). Check your current frame number and stop when current frame become equal QMovie::frameCount()
m_movie = new QMovie(":/gif/tenor.gif");
connect(m_movie, SIGNAL(frameChanged(int)),
this, SLOT(OnFrameChanged(int)));
ui->lblMovie->setMovie(m_movie);
m_movie->start();
And in slot:
void MainWindow::OnFrameChanged(int frame)
{
if (frame == m_movie->frameCount() - 1) {
m_movie->stop();
}
}
I am trying to draw mesh Topology (graph) in sprite kit using swift. I am new for sprite kit . Please give any suggestion or sample code.
So as I mentioned in the comments, I don't know how to make a perfect topography algorithm, however I did come up with something that can replicate you picture.
Basically you add a bunch of plots as SKNodes, then use the .position property to use as the start and end points for a line drawn with CGPath. From that path, you can create a SKShapeNode(path: CGPath).
I also added a custom button in here that uses delegation, but it is completely separate from the actual "guts" of the topography. It's just a button.
// Overly complex way of creating a custom button in SpriteKit:
protocol DrawLinesDelegate: class { func drawLines() }
// Clickable UI element that will draw our lines:
class DrawLinesButton: SKLabelNode {
weak var drawLinesDelegate: DrawLinesDelegate?
init(text: String, drawLinesDelegate: DrawLinesDelegate) {
super.init(fontNamed: "Chalkduster")
self.drawLinesDelegate = drawLinesDelegate
self.text = text
isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print(drawLinesDelegate)
drawLinesDelegate?.drawLines()
}
required init?(coder aDecoder: NSCoder) { fatalError("") }
override init() { super.init() }
};
class GameScene: SKScene, DrawLinesDelegate {
var plots = [SKShapeNode]()
// var lines = [SKShapeNode]() // This may be useful in a better algorithm.
var nodesDrawnFrom = [SKShapeNode]()
var drawLinesButton: DrawLinesButton?
func drawLine(from p1: CGPoint, to p2: CGPoint) {
let linePath = CGMutablePath()
linePath.move(to: p1)
linePath.addLine(to: p2)
let line = SKShapeNode(path: linePath)
line.strokeColor = .red
line.lineWidth = 5
// lines.append(line) // Again, may be useful in a better algo.
addChild(line)
}
func drawLines() {
// Remove all lines: // Again again, may be useful in a better algorithm.
/*
for line in lines {
line.removeFromParent()
lines = []
}
*/
// The plot that we will draw from:
var indexNode = SKShapeNode()
// Find indexNode then draw from it:
for plot in plots {
// Find a new node to draw from (the indexNode):
if nodesDrawnFrom.contains(plot) {
continue
} else {
indexNode = plot
}
// Draw lines to every other node (from the indexNode):
for plot in plots {
if plot === indexNode {
continue
} else {
drawLine(from: indexNode.position, to: plot.position)
nodesDrawnFrom.append(indexNode)
}
}
}
}
func addNode(at location: CGPoint) {
let plot = SKShapeNode(circleOfRadius: 50)
plot.name = String(describing: UUID().uuid)
plot.zPosition += 1
plot.position = location
plot.fillColor = .blue
plots.append(plot)
addChild(plot)
}
override func didMove(to view: SKView) {
drawLinesButton = DrawLinesButton(text: "Draw Lines", drawLinesDelegate: self)
drawLinesButton!.position.y = frame.minY + (drawLinesButton!.frame.size.height / 2)
addChild(drawLinesButton!)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = touches.first!.location(in: self)
addNode(at: location)
}
}
imperfect algo:
Here you can see that there are multiple lines being drawn from one to another when you had mid-points (that is something blocking a straight line):
You would need to add another entire section to the algorithm to check for this.
Another important thing to note is that SKShapeNode() is very unperformant, and it would be best to transform all of these to SpriteNodes, or to bit-blit the entire scene onto a static texture.
However, having them all as ShapeNodes give you the most flexibility, and is easiest to explain here.
Generalities : explanations about my program and its functioning
I am working on a photo-retouching JavaFX application. The final user can load several images. When he clicks on the button REVERSE, a Task is launched for each image using an Executor. Each of these Task executes the reversal algorithm : it fills an ArrayBlockingQueue<Pixel> (using add method).
When the final user clicks on the button REVERSE, as I said, these Task are launched. But just after these statements, I tell the JavaFX Application Thread to draw the Pixel of the ArrayBlockingQueue<Pixel> (using remove method).
Thus, there are parallelism and concurrency (solved by the ArrayBlockingQueue<Pixel>) between the JavaFX Application Thread and the Task, and between the Task themselves.
To draw the Pixel of the ArrayBlockingQueue<Pixel>, the JavaFX Application Thread starts an AnimationTimer. The latter contains the previously-mentionned remove method. This AnimationTimer is started for each image.
I think you're wondering yourself how this AnimationTimer can know to what image belongs the Pixel it has removed ? In fact, each Pixel has an attribute writable_image that specifies the image to what it belongs.
My problems
Tell me if I'm wrong, but my program should work. Indeed :
My JavaFX Application Thread is the only thread that change the GUI (and it's required in JavaFX) : the Task just do the calculations.
There is not concurrency, thanks to the BlockingQueue I use (in particular, there isn't possibility of draining).
The AnimationTimer knows to what image belongs each Pixel.
However, it's (obviously !) not the case (otherwise I wouldn't have created this question haha !).
My problem is that my JavaFX Application freezes (first problem), after having drawn only some reversed pixels (not all the pixels). On the last loaded image moreover (third problem).
A detail that could be the problems' cause
But I would need your opinion.
The AnimationTimer of course doesn't draw the reversed pixels of each image directly : this is animated. The final user can see each pixel of an image being reversed, little by little. It's very practical in other algorithms as the creation of a circle, because the user can "look" how the algorithm works.
But to do that, the AnimationTimer needs to read a variable called max. This variable is modified (writen) in... each Task. But it's an AtomicLong. So IF I AM NOT WRONG, there isn't any problem of concurrency between the Task themselves, or between the JavaFX Application Thread and these Task.
However, it could be the problem : indeed, the max's value could be 2000 in Task n°1 (= in image n°1), and 59 in Task n°2 (= in image n°2). The problem is the AnimationTimer must use 2000 for the image n°1, and 59 for the n°2. But if the Task n°1 et n°2 have finished, the only value known by the AnimationTimer would be 59...
Sources
When the user clicks on the button REVERSE
We launch the several Task and start several times the AnimationTimer. CLASS : RightPane.java
WritableImage current_writable_image;
for(int i = 0; i < this.gui.getArrayListImageViewsImpacted().size(); i++) {
current_writable_image = (WritableImage) this.gui.getArrayListImageViewsImpacted().get(i).getImage();
this.gui.getGraphicEngine().executor.execute(this.gui.getGraphicEngine().createTask(current_writable_image));
}
for(int i = 0; i < this.gui.getArrayListImageViewsImpacted().size(); i++) {
current_writable_image = (WritableImage) this.gui.getArrayListImageViewsImpacted().get(i).getImage();
this.gui.getImageAnimation().setWritableImage(current_writable_image);
this.gui.getImageAnimation().startAnimation();
}
The Task are part of the CLASS GraphicEngine, which contains an Executor :
public final Executor executor = Executors.newCachedThreadPool(runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(true);
return t ;
});
public Task createTask(WritableImage writable_image) {
int image_width = (int) writable_image.getWidth(), image_height = (int) writable_image.getHeight();
Task ret = new Task() {
protected Void call() {
switch(operation_to_do) {
case "reverse" :
gui.getImageAnimation().setMax(image_width*image_height); // USE OF "MAX" VARIABLE
reverseImg(writable_image);
break;
}
return null;
}
};
return ret;
}
The same CLASS, GraphicEngine, also contains the reversal algorithm :
private void reverseImg(WritableImage writable_image) {
int image_width = (int) writable_image.getWidth(), image_height = (int) writable_image.getHeight();
BlockingQueue<Pixel> updates = gui.getUpdates();
PixelReader pixel_reader = writable_image.getPixelReader();
double[] rgb_reversed;
for (int x = 0; x < image_width; x++) {
for (int y = 0; y < image_height; y++) {
rgb_reversed = PhotoRetouchingFormulas.reverse(pixel_reader.getColor(x, y).getRed(), pixel_reader.getColor(x, y).getGreen(), pixel_reader.getColor(x, y).getBlue());
updates.add(new Pixel(x, y, Color.color(rgb_reversed[0], rgb_reversed[1], rgb_reversed[2], pixel_reader.getColor(x, y).getOpacity()), writable_image));
}
}
}
Finally, here is the code of the CLASS AnimationTimer. There is nothing particular. Note the variable max is used here too (and in the CLASS GraphicEngine : setMax).
public class ImageAnimation extends AnimationTimer {
private Gui gui;
private AtomicLong max, speed, max_delay;
private long count, start;
private WritableImage writable_image;
ImageAnimation (Gui gui) {
this.gui = gui;
this.count = 0;
this.start = -1;
this.max = new AtomicLong(Long.MAX_VALUE);
this.max_delay = new AtomicLong(999_000_000);
this.speed = new AtomicLong(this.max_delay.get());
}
public void setMax(long max) {
this.max.set(max);
}
public void setSpeed(long speed) { this.speed.set(speed); }
public double getMaxDelay() { return this.max_delay.get(); }
#Override
public void handle(long timestamp) {
if (start < 0) {
start = timestamp ;
return ;
}
ArrayList<Pixel> list_sorted_pixels = new ArrayList<>();
BlockingQueue<Pixel> updates = this.gui.getUpdates();
for(Pixel new_pixel : updates) {
if(new_pixel.getWritableImage() == writable_image) {
list_sorted_pixels.add(new_pixel);
}
}
while (list_sorted_pixels.size() > 0 && timestamp - start > (count * this.speed.get()) / (writable_image.getWidth()) && !updates.isEmpty()) {
Pixel update = list_sorted_pixels.remove(0);
updates.remove(update);
count++;
if (update.getX() >= 0 && update.getY() >= 0) {
writable_image.getPixelWriter().setColor(update.getX(), update.getY(), update.getColor());
}
}
if (count >= max.get()) {
this.count = 0;
this.start = -1;
this.max.set(Long.MAX_VALUE);
stop();
}
}
public void setWritableImage(WritableImage writable_image) { this.writable_image = writable_image; }
public void startAnimation() {
this.start();
}
}