2013/12/27

QTableViewでTable内のRowをDrag&DropでMoveだけさせる

年明けにやるんだけど忘れないようにメモ。

Qtで表とかSpreadsheetを追加したい場合はQTableViewというのを使うのが便利なんだけど、 Drag&DropでRowの位置を変更したいという場合結構ありそうな気がするんだけどもわりとやり方がめんどくさい。

以下のサイトでも質問されているけど自分がやりたい方法で実現してない

2番めの投稿にもある通り、QTableView::verticalHeader()->setMovable(true)(setSelectionMovableは古い)すれば、 表のセルでなくてHeaderをDrag&DropすることでMoveできる。 これでもいいんだけど、やっぱりセル自体をDrag&Dropできたほうが直感的。という事で追加で調べてみた。

使うのはQTableViewとQStandardItemModelとQStandardItemというQTableViewを使う場合には もっともポピュラーだと思われる方法。

QTableViewの配置はQt Designerで↓のように配置して

QTableViewの設定から以下の項目をセット

key value

QWidget::acceptDrops

true

QAbstractItemView::dragEnabled

true

QAbstractItemView::dragDropOverwriteMode

false

QAbstractItemView::dragDropMode

InternalMove

QAbstractItemView::defaultDropAction

MoveAction

QAbstractItemView::selectionBehavior

SelectRows

一つ一つON/OFFしてないので不要なものも入ってるかも。

一見上記の設定だけでもMoveだけを許可してdragDropOverwriteModeをfalseにしてるのでうまくいきそうな感じがするんだが、 このままQStandardItemをセットするとDrag&Dropでセルとセルの間を選ぶとMoveされるんだけど、 セル自体を選ぶと上書きされてしまう。これを防ぐ方法がわからなくて困った。

ヒントになったのは以下

Note: This is not intended to prevent overwriting of items. The model's implementation of flags() should do that by not returning Qt::ItemIsDropEnabled.

”これ(=dragDropOverwriteMode)はitemを上書きするのを防がない。flags()の実装でQt::ItemIsDropEnabledを返さない事で上書きを防げる。”

ということはQStandardItemModelを継承してflags()をreimplementしてQt::ItemIsDropEnabledのFlagを下ろせばいいのかとおもったんだが・・

以下のようにMyItemModelを作ってQStandardItemModelの代わりにnewしてQTableView::setModel()しても現象は変わらない・・。

class MyItemModel : public QStandardItemModel
{
public:
  MyItemModel(QObject * parent = 0) : QStandardItemModel(parent){}
  MyItemModel(int rows, int columns, QObject * parent = 0) : QStandardItemModel(rows, columns, parent) {}
  
  virtual Qt::ItemFlags flags()const{
    return (QStandardItemModel::flags() & ~Qt::ItemIsDropEnabled);
  }
};

うーん。

じゃあModelをQStandardItemModelに戻して今度は以下のように各QStandardItem::setFlagでQt::ItemIsDropEnabledをOFFしてみたら Item同士の上書きがなくなった!

QStandardItem *item = new QStandardItem(QString("item00"));
item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled);
mpModel->setItem(0, 0, item);

こんな感じ。

なんかMoveした瞬間に一瞬一列増えて消えてをしてるような気もするが・・。


2013/12/18

QCustomPlotで凡例をグラフの外に表示→ちょっとめんどい

凡例はデフォルトではグラフ領域の中に表示される+設定で場所を指定できるみたいだが、 例えば入力を次々変えていってグラフを表示したい場合とか、グラフがどの領域にくるかわからない場合があるんだが。

そんな場合凡例はグラフ内でなくてグラフ外に表示しておいてほしいのでそのやり方を書いてみる。 結果、今の所設定一発っていうわけではなくて以下のページにもあるみたいにLayoutを調整して対応しないと いけないので、たかが凡例をグラフ外にもっていく割にはめんどい印象・・。

以下のQCPLegendのページにも以下のように書いてある。

To move the legend to another position inside the axis rect, use the methods of the QCPLayoutInset. To move the legend outside of the axis rect, place it anywhere else with the QCPLayout/QCPLayoutElement interface.

凡例を軸の内側に動かすためにはQCPLayoutInsetのmehodを使え。外に動かしたい場合はQCPLayout/QCPLayoutElementを使って外に配置しろ。 みたいなことをいってるので多分合ってると思うんだけど。

つまり例えば↓のようにQCPLayoutを使って自分で凡例を配置せよということらしい。


↑を実現するには、

  1. もともと用意されているグラフを描画するLayoutにColumnを追加してQCPLayoutGridを配置し
  2. そこに3つのRowを追加して上下にPadding用のQCPLayoutElementを配置し
  3. 中央にQCPLegendを配置

    という感じ。例によってHistogramを例にしてコードにすると

  // When initialize QCustomPlot

  // Moved legend to outside of axis rect
  QCPLayoutGrid *subLayout = new QCPLayoutGrid;  // グラフの右に挿入されるQCPLayoutGrid(subLayout)
  ui.histogram->plotLayout()->addElement(0, 1, subLayout); // (Row, Column) = (0,1)にsubLayoutを追加
  subLayout->addElement(0, 0, new QCPLayoutElement); // subLayoutの(Row, Column) = (0,0)にPadding用QCPLayoutElementを
  subLayout->addElement(1, 0, ui.histogram->legend); // (Row, Column) = (1,0)に凡例(Legend)を追加
  subLayout->addElement(2, 0, new QCPLayoutElement); // 同Paddingを追加
  subLayout->setColumnStretchFactor(0, 0.01);        // subLayoutの横幅はなるべく小さくする
  subLayout->setRowStretchFactor(1, 0.01);           // subLayoutの凡例がはいるRow=1はなるべく小さくする
  ui.histogram->plotLayout()->setColumnStretchFactor(1, 0.01); // subLayoutが入るColumn=1の横幅はなるべく小さくする



もし、上記の設定なしだと以下の様に表示されるが、



上記のようにLayout設定すると以下のようになる。

なんとかグラフ領域の外にもっていけたけど、凡例の右側が切れてたりしてちょっとしっくりこないきもするなぁ。


2013/12/17

Bloggerでの投稿をmarkdownで書く

Bloggerのエディタが使いにくくて投稿しにくいのでなんとかmarkdownでできないかなと思い、 色々調べたら、いくつか参考にさせてもらったけどここ↓を主に参考にさせてもらってmarkdown対応してみました。

基本は、

  • htmlを直接記入するエディタモードを使う。
  • markdown全体をtextareaで囲み、class="markdown"に割り当てる
  • Javascriptでtext文を取得してshowdown.js等のmarkdown -> HTML変換をして置き換える

という方針みたいです。

詳細はリンク先をみてもらうとして、 自分の環境では以下の変更を加えました。

トップページで複数の投稿が並んでいる際に、1番目の投稿以外はtextareaがそのまま表示されてしまう

リンク先のサイトだと問題無く表示されていたのですが、自分の環境ではなぜか 一番先頭(最新)の投稿は正しく表示されるが2番目以降の投稿はtextareaにmarkdownでかかれた内容が、こんな感じ↓でそのまま表示されている状態になってしまいました。

リンク先には↓の“HTMLの編集“からの前に以下のjavascriptを挿入するとなってました。

<script src="/path/to/showdown.js"></script>
<script>
var sd = new Showdown.converter(),
      mdEl = document.querySelector('.markdown'),
      converted = document.createElement('div');

converted.className = 'showdown';
converted.innerHTML = sd.makeHtml(mdEl.value);
mdEl.parentNode.replaceChild(converted, mdEl);
</script>

でも、自分環境だとどうもダメ(テンプレートのせい?)で、いろいろと試行錯誤した結果以下のようにしました。

<script src='/PATH TO/showdown.js'/>
<script type='text/javascript'>
  var sd = new Showdown.converter();
  var mdEls = document.querySelectorAll(".markdown");
  for(var i = 0; i < mdEls.length; i++){
    converted = document.createElement('div');
    converted.className = 'showdown';
    converted.innerHTML = sd.makeHtml(mdEls[i].value);
    mdEls[i].parentNode.replaceChild(converted, mdEls[i]);
    window.setTimeout(function() {
      document.body.className = document.body.className.replace('loading', '');
    }, 10);
  }
</script>

違いを要約すると、querySelector()の代わりにquerySelectorAll()を使って textareaをNodeListで取得して全てのNodeに対してshowdownのmakeHtml()をかけて置き換えてるだけです。

最終的にはshowdown.jsのextensionのtable.jsを使って(showdownはデフォルトではtableの記述に未対応)以下のようにしました。

<script src='/PATH TO/showdown.js'/>
<script src='/PATH TO/extensions/table.js'/>
<script type='text/javascript'>
  var sd = new Showdown.converter({extensions: ['table']});
  var mdEls = document.querySelectorAll(".markdown");
  for(var i = 0; i < mdEls.length; i++){
    converted = document.createElement('div');
    converted.className = 'showdown';
    converted.innerHTML = sd.makeHtml(mdEls[i].value);
    mdEls[i].parentNode.replaceChild(converted, mdEls[i]);
    window.setTimeout(function() {
      document.body.className = document.body.className.replace('loading', '');
    }, 10);
  }
</script>

注意点としてはextensionのtable.jsは先頭に|がないと表にならない事。

ダメ

First Header | Second Header | Third Header
------------ | ------------- | ------------
Content Cell | Content Cell | Content Cell

イケる

| First Header | Second Header | Third Header |
| ------------ | ------------- | ------------ |
| Content Cell | Content Cell | Content Cell |

これ以外はprettify.jsでのコードハイライトなども以下を参考にさせてもらいました。 どうもありがとうございます(^^)

最後に一点だけ注意点ですが、以下の歯車アイコンでモバイル向けの設定をカスタムにしないと、スマホで見た時にやっぱりtextareaがそのまま表示されちゃうので注意。

以下をカスタムにする。

結構たいへんだったけどやっぱしmarkdownでかけるとすいすい思いのままに書けてとってもグー!


2013/12/16

QCustomPlotで凡例をクリックしてグラフの表示をToggle

複数のグラフを表示していて、見たいグラフが他のグラフとかぶっててじゃまなんで手前のグラフを 消したいなんて結構あると思う。

そんな時よくあるやりかたとして凡例から対象グラフをクリックするとそれが消えるみたいな。 そのやり方を書いてみる。わりと簡単だけど。

まずは凡例(legend)を表示する方法。QCustomPlotの初期化時に

    // histogram=QCustomPlot
    ui.histogram->setAutoAddPlottableToLegend(true);// plottable(=graph)を追加したら自動で凡例に追加
    ui.histogram->legend->setVisible(true); // 凡例を表示する

    for(int i = 0; i < numColorHistogram; i++){
        // QCPBars* mpHistogrambars[];
        mpHistogramBars[i] = new QCPBars(ui.histogram->xAxis, ui.histogram->yAxis);
        mpHistogramBars[i]->setName(CHART_NAMES[i]);
        mpHistogramBars[i]->setPen(QPen(CHART_COLORS[i]));
        mpHistogramBars[i]->setBrush(QBrush(CHART_COLORS[i]));
        ui.histogram->addPlottable(mpHistogramBars[i]);// graphをQCustomPlotに追加
    }

    // 凡例がクリックされたらslot toggleVisibilityを呼ぶようにconnect
    connect(ui.histogram, SIGNAL(legendClick(QCPLegend*,QCPAbstractLegendItem*,QMouseEvent*)), this, SLOT(toggleVisibility(QCPLegend*,QCPAbstractLegendItem*,QMouseEvent*)));




そしてtoogleVisibilityで以下のようにvisibilityを変更

void MainWindow::toggleVisibility(QCPLegend* legend, QCPAbstractLegendItem* item, QMouseEvent* event){
    if(!item) return;
    QCPPlottableLegendItem *plItem = qobject_cast<QCPPlottableLegendItem*>(item);
    if(!plItem) return;

    plItem->plottable()->setVisible(!plItem->plottable()->visible());
}




これで凡例の対象グラフをクリックするとそのグラフの表示・非表示がtoggleされる。こんな感じ↓



気になる点はlegendをみてどのgraphが現在表示されているのがわからない点。これはまた調査しよう。


2013/12/15

QCustomPlotでマウスホバー時に値を表示

前回QCustomPlotがいい感じでグラフ表示の基本を書いてみたので 次はマウスホバー+グラフ値の表示方法について書いてみる。

マウスホバーはQCPItemTracerをつかう。 QCustomPlotは横軸のことをkeyAxis, 縦軸をvalueAxisと呼ぶがQCPMouseTrace::setGraphKey(int key)でkeyを 更新すると、対応するvalue値を保持して、指定したindicatorでグラフ内の対象箇所を示してくれる機能がある。

でも一点難点があるのはQCPGraphにだけ対応していてQCPBarsには対応してない点。 なので前回あげたヒストグラムには適応できない。

というわけで、ちょっと強引にQCPItemTracerにsetBars()とsetBarsKey()を追加してメンバー変数にQCPBarsを持たせてみる。

  void setBars(QCPBars *bars);
  void setBarsKey(double key);

  QCPBars  *mBars;
void QCPItemTracer::setBars(QCPBars *bars){
    if(bars)
    {
        if(bars->parentPlot() == mParentPlot){
            position->setType(QCPItemPosition::ptPlotCoords);
            position->setAxes(bars->keyAxis(), bars->valueAxis());
            mBars = bars;
            updatePosition();
        } else
            qDebug() << Q_FUNC_INFO << "bars isn't in same QCustomPlot instance as this item";
    } else
    {
        mBars = 0;
    }
}

void QCPItemTracer::setBarsKey(double key){
    setGraphKey(key);
}

// QCPItemTracer::updatePosition()の末尾に以下を追加

  else if(mBars){
      if(mBars->data()->size() > 1){
          QCPBarDataMap::const_iterator first = mBars->data()->constBegin();
          QCPBarDataMap::const_iterator last  = mBars->data()->constEnd()-1;
          if (mGraphKey < first.key())
              position->setCoords(first.key(), first.value().value);
          else if (mGraphKey > last.key())
              position->setCoords(last.key(), last.value().value);
          else
          {
              QCPBarDataMap::const_iterator it = mBars->data()->lowerBound(mGraphKey);
              if (it != first) // mGraphKey is somewhere between iterators
              {
                  QCPBarDataMap::const_iterator prevIt = it-1;
                  if (mInterpolating)
                  {
                      // interpolate between iterators around mGraphKey:
                      double slope = (it.value().value-prevIt.value().value)/(it.key()-prevIt.key());
                      position->setCoords(mGraphKey, (mGraphKey-prevIt.key())*slope+prevIt.value().value);
                  } else
                  {
                      // find iterator with key closest to mGraphKey:
                      if (mGraphKey < (prevIt.key()+it.key())*0.5)
                          it = prevIt;
                      position->setCoords(it.key(), it.value().value);
                  }
              } else // mGraphKey is exactly on first iterator
                  position->setCoords(it.key(), it.value().value);
          }
      }else if (mBars->data()->size() == 1)
      {
          QCPBarDataMap::const_iterator it = mBars->data()->constBegin();
          position->setCoords(it.key(), it.value().value);
      } else
          qDebug() << Q_FUNC_INFO << "graph has no data"; //Add a comment to this line
  }



これでQCPItemTracerでQCPBarsの値を保持するようになった(はず)。
でもってQCPCustomPlotでグラフを描画したいクラスで以下のようにQItemTracerを初期化する。

    mpMouseTracer = new QCPItemTracer(ui.histogram);
    ui.histogram->addItem(mpMouseTracer);// Graph上に表示を設定(histogram = QCustomPlot)
    mpMouseTracer->setBars(mpHistogramBars);// ここ以外はオリジナルQCPItemTracerと同じ
    mpMouseTracer->setGraphKey(0.0);// 初期key
    mpMouseTracer->setStyle(QCPItemTracer::tsCircle);// 対象点を●で表示他にもCrossなどがある
    mpMouseTracer->setPen(QPen(Qt::red));// ●の線の色
    mpMouseTracer->setBrush(Qt::red);// ●の色
    mpMouseTracer->setSize(7);//●の大きさ

    // 今回はグラフマウスホバーで値を表示したいのでQCustomPlot::mouseMove signal時にslotを読んでもらう
    connect(ui.histogram, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(histogramMouseMoved(QMouseEvent*)));
void MainWindow::histogramMouseMoved(QMouseEvent *event){
    QPoint pos = event->pos();
    double key = floor(mpHistogramBars->keyAxis()->pixelToCoord(pos.x()));
    mpMouseTracer->setBarsKey(key); // 最新のkey値をセット
    ui.histogram->replot();// repolotして初めて新しいポジションに●が移動する
}



とすると↓のように●でマウスホバー点をTraceしてくれる。



テキスト(key, value)の表示にはQCPItemTextを使う。 mpMouseTracerと同様にこんな感じで設定して・・

    mpMouseText = new QCPItemText(ui.histogram);
    ui.histogram->addItem(mpMouseText);
    mpMouseText->position->setType(QCPItemPosition::ptAxisRectRatio);
    mpMouseText->setFont(QFont(font().family(), 12));
    mpMouseText->setPositionAlignment(Qt::AlignLeft);
    mpMouseText->position->setCoords(QPointF(0, 0));
    mpMouseText->setText("(-, -)");



histogramMouseMoved slotで更新する。

void MainWindow::histogramMouseMoved(QMouseEvent *event){
    QPoint pos = event->pos();
    double key = floor(mpHistogramBars->keyAxis()->pixelToCoord(pos.x()));
    mpMouseTracer->setBarsKey(key);
    double x, y;
    // 以下は文字がグラフ外に出て行かないように適当にclipしている
    x = qBound(0., mpMouseTracer->position->key()   / mpHistogramBars->keyAxis()->range().size(), 0.75);
    y = qBound(0., 1. - mpMouseTracer->position->value() / mpHistogramBars->valueAxis()->range().size(), 0.9);// value  coord is TopLeft origin
    mpMouseText->position->setCoords(x, y);
    mpMouseText->setText("(" + QString::number(mpMouseTracer->position->key()) + ", " + QString::number(mpMouseTracer->position->value()) + ")");
    ui.histogram->replot();
}



これでOK.


UPDATE: こんな感じ↓



参考までに

* tsCrosshair



* tsPlug



* tsSquare


2013/12/12

QCustomPlotがいい感じ

最近色々な機会でQtを使う機会が増えたんだが、 いろいろ使ってるうちに手軽にグラフを描画したいケースがけっこうでてきた。

これまではQWTを使っていたんだが、 単純な棒グラフ作るだけでもわりと手続きが多くてめんどくさいのが難点。 あとドキュメントもいまいち。

で、代わりを探してみるとQCustomPlotっていうのを 最近よく見かけるようになってて、わりと評判もよさそうなんで使ってみた。

準備としてはQCustomPlotにアクセスしてDownloadのページから 最新のパッケージをダウンロードし、その中から

  • qcustomplot.h
  • qcustomplot.cpp

を持ってきてプロジェクトに追加するだけ。

まず、Qt Designerでグラフを表示させたい箇所にWidget(QWidget)を配置。 配置したら、右クリックして格上げ先を指定を選択↓



格上げ先にQCustomPlotとqcostomplot.hを指定して「追加」を押し



追加されたQCustomPlotを選択して「格上げ」。



名前を例えばhistogramとかにしてDesignerでの作業は完了。



例えばヒストグラムを表示させたい場合にはQCPBarsを使う

クラスメンバにQCPBars(QCPBars *mpHistogramBars)を追加し、コンストラクタでのように初期化する。

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    mpHistogramBars = new QCPBars(ui.histogram->xAxis, ui.histogram->yAxis);
    mpHistogramBars->setPen(QPen(Qt::gray));// 色指定
    mpHistogramBars->setBrush(QBrush(Qt::gray));// 色指定
    ui.histogram->addPlottable(mpHistogramBars);// 追加
}



あとは、描画したいタイミングで以下のようにQVectorを使ってデータをセットする。

void MainWindow::drawHistogram(const QImage &image){
    QVector<double> keys(256);
    QVector<double> values(256);
    for(int i = 0; i < keys.size(); i++){
        keys[i] = i;
        values[i] = 0;
    }
    for(int j = 0; j < image.height(); j++){
        for(int i = 0; i < image.width(); i++){
            QRgb rgb = image.pixel(i, j);// RGB -> Y
            int y = qBound(0, (int)(0.299 * qRed(rgb) + 0.587 * qGreen(rgb) + 0.114 * qBlue(rgb) + 0.5), 255);
            values[y] += 1.0;
        }
    }
    mpHistogramBars->setData(keys, values);
    ui.histogram->rescaleAxes();
    ui.histogram->replot();
}



QCustomPlotでは基本的にデータはdoubleで指定するもよう。 実行すると以下のような感じのヒストグラムが表示される。 (ただし、グラフ中の赤丸と座標点は表示されない。結構グラフを表示した後にマウスホバーで値を見たい場合はあるので、これの表示の仕方はまた別途)



結構見栄えがいい。 複数表示もいい感じにできます。

QWTだとsetIntervalだとか結構わかりにくいコードをいっぱいかかないといけなかった・・。 それとくらべてQCustomPlotは直感的でわかりやすいし、簡単!

次はQItemTracerでマウスホバーのやり方でもまとめる予定。


2013/08/20

rvm install等々


たまにしか入れないので毎回忘れて検索するのがわずらわしいのでメモ。
基本ここに書いてあるSingle User Installations → https://rvm.io/rvm/install

$ \curl -L https://get.rvm.io | bash -s stable

でインストール完了。sudoでインストールしないこと。
.bashrcにrvmへのPathを通す コードが入っているのでsourceで有効にする。

$rvm list known
でインストール可能なバージョン確認
 $rvm install 1.8.7
とかでインストール
$rvm use 1.8.7 --default
で、デフォルトrvm以下の1.8.7 のrubyが使われる。
$gem list
で、install済みのgemの一覧を確認
$gem install bundler
でinstall