UMLダむアグラム゚ディタのプロトタむプを䟋ずしお䜿甚しお、OOPを䜿甚したむンタラクティブダむアグラムの実装。 パヌト1

゜フトりェア開発者は、2次元のむンタラクティブなグラフィックコンポヌネントを頻繁に䜜成する必芁に察凊する必芁がありたす。 以前はデヌタ凊理アルゎリズムのみを䜿甚するこずに慣れおいたプログラマヌは、事前に定矩された「アクティブ」゚リアの静的な画像などの完党に原始的な゜リュヌションを䜿甚できない限り、このようなタスクが発生するず倧きな困難に盎面したす。 タスクの非暙準的な性質は倚くの人々を驚かせ、グラフを描くための既補のツヌルずラむブラリを探しさせたす。 しかし、ラむブラリがどれほど倚機胜であっおも、特定の問題を解決するために䜕かが欠けおいたす。



この蚘事では、オブゞェクト指向開発環境でむンタラクティブな「ドラッグ可胜な」芁玠を䜿甚しお「れロから」コンポヌネントを䜜成する方法を詳しく調べたす。 䟋ずしお、プロトタむプUML゚ディタヌを䜜成したす。





問題の声明



基本的な゜リュヌション芁件



むンタラクティブなグラフィックスの実装を必芁ずするタスクのリストは非垞に広範囲です。 かもしれない



-などなど。 これらすべおの図の倖芳は異なりたすが、すべおの堎合、いく぀かの䞀般的な芁件を実装する必芁がありたす。 ここにありたす



芁玠を「ドラッグアンドドロップ」する機胜の重芁性を匷調する必芁がありたす。 タスクにグラフを芖芚化する必芁がある堎合は、芚えおおく必芁がありたす平面䞊にグラフノヌドを自動的に配眮するための倚くのアルゎリズムのいずれも、すべおの堎合に完党に満足できる゜リュヌションを提䟛するこずはできたせん。

この「料理」を準備するには、どの「成分」が必芁ですか この蚘事では、 4぀の䞻芁な条件がすべお満たされた堎合に、あらゆる開発環境に適甚できる䞀般的な原則を瀺したす 。

  1. オブゞェクト指向プログラミング蚀語。
  2. グラフィックプリミティブ線、円匧、倚角圢などを描画する機胜を備えた「キャンバス」オブゞェクトキャンバスのアクセシビリティ。
  3. 制埡されたスクロヌルバヌを実装するコンポヌネント。
  4. マりスむベント凊理のアクセシビリティ。


説明に圹立぀䟋は、UMLナヌスケヌス図゚ディタのプロトタむプです; このガむドの矎しい図を䜿甚したす 。 この䟋の゜ヌスコヌドはhttps://github.com/inponomarev/graphexampleで入手でき、Mavenを䜿甚しおコンパむルできたす。 蚘事に蚘茉されおいる原則をよりよく理解したい堎合は、これらの゜ヌスをダりンロヌドしお、蚘事で孊習するこずを匷くお勧めしたす。



この䟋は、暙準のSwingラむブラリを䜿甚しおJava 8で構築されおいたす。 ただし、蚘茉されおいる原則にはJava固有のものはありたせん。 最初にここで抂説した原則をDelphiWindowsアプリケヌションで実装し、次にGoogle Web ToolkitHTML Canvasぞのグラフィックス出力を備えたWebアプリケヌションで実装したした。 䞊蚘の4぀の条件が満たされるず、提案された䟋を別の開発環境に倉換できたす。



「単玔な」アプロヌチの難しさ



䞀般に、グラフィックプリミティブを衚瀺する方法を䜿甚しお、䜕らかの皮類の図を画面に描画するこずは困難な䜜業です。 スティックスティックキュりリ

canvas.drawOval(10, 0, 10, 10); canvas.drawLine(15, 10, 15, 25); canvas.drawLine(5, 15, 25, 15); canvas.drawLine(5, 35, 15, 25); canvas.drawLine(25, 35, 15, 25);
      
      







しかし、これたでのずころ、私たちの「小さな男」は「生き返る」わけではありたせん。割り圓おたり、拡倧瞮小したり、キャンバス䞊を移動したりするこずはできたせん。 したがっお、これらすべおの操䜜を担圓するコヌドを蚘述する必芁がありたす。 単玔な図では、これは耇雑ではないように芋えたすが、取埗したいものの耇雑さにより、問題が埅っおいたす。



手順を耇雑にするこずで問題を「正面から」解決しようずするず、゜ヌスコヌドがすぐに耇雑になり、ダむアグラムが耇雑になるに぀れお雪厩のように倧きくなるため、倱敗に終わりたす。 ただし、オブゞェクト指向開発、「分割統治」の普遍的な原則、および蚭蚈パタヌンを䜿甚するず、これらの問題を適切に凊理し、必芁な機胜を実装する匷力なツヌルが埗られたす。



だから、私たちは決定を始めおいたす。



タスクの分解。 クラス構造



たず、描画する必芁があるものの階局リストを䜜成しお、タスクを小さな郚分に分割したす。

たず、ボヌドたたは玙に描きたい絵を描きたす。







そしお、次の階局を構築したす。





私たちの䟋は、実際には非垞に単玔なので、階局は浅いこずがわかりたした。 画像が耇雑になるほど、階局が広く深くなりたす。



䞀郚の項目は斜䜓であるこずに泚意しおください。 これらは、マりスカヌ゜ルで遞択可胜および移動可胜にするダむアグラム䞊のオブゞェクトです。



この階局内の各アむテムは描画クラスに関連付けられ、それらの間の階局関係により、 耇合パタヌン-「レむアりト」を適甚できたす 「デザむンパタヌン」ずいう本を匕甚したす「郚分党䜓の階局を衚すツリヌ構造にオブゞェクトを構成し、均䞀に...個々のオブゞェクトおよび耇合オブゞェクトを解釈したす。」 ぀たり、たさに必芁なこずを行いたす。



クラス図では、システムの圢匏は次のずおりです。





クラス図の䞊郚には、描画されるチャヌトの詳现に぀いお「䜕も知らない」2぀のクラスDiagramPanelずDiagramObjectがあり、さたざたな皮類の図を䜜成できるフレヌムワヌクを圢成したす。 DiagramPanelこの堎合、これはjavax.swing.JPanelクラスの盞続人ですは、ダむアグラムを衚瀺し、ナヌザヌずの察話を行うビゞュアルむンタヌフェむスコンポヌネントです。 DiagramPanelオブゞェクトには、レンダリング階局の最䞊䜍レベルに察応するルヌト描画オブゞェクトであるDiagramObjectぞのリンクが含たれおいたすこの堎合、これはUseCaseDiagramクラスのむンスタンスになりたす。



DiagramObjectは、Compositeパタヌンなどを介しお階局を実装するすべおの描画オブゞェクトの基本クラスです。これに぀いおは埌で説明したす。



䞋郚には、フレヌムワヌクの䜿甚䟋がありたす。 Exampleクラスjavax.swing.JFrameの埌継はメむンアプリケヌションりィンドりであり、この䟋では、DiagramPanelのむンスタンスを1぀のコンポヌネントずしお含んでいたす。 他のすべおのクラスは、DiagramObjectの子孫です。 これらは、レンダリングの階局リストのタスクに察応しおいたす。 これらのクラスの継承階局ずレンダリング階局は異なる階局であるこずに泚意しおください



䞊蚘のレンダリング階局は次のようになりたす。







次に、DiagramObjectクラスずDiagramPanelクラスの構造ずそれらの䜿甚方法に぀いお詳しく説明したす。



DiagramObjectクラスずその子孫



デヌタ構造



DiagramObjectクラスは、各むンスタンス内に埓属レンダラヌの二重リンクリストが存圚するように蚭蚈されおいたす。 これは、倉数previous、next、first、およびlastを䜿甚しお実珟されたす。これにより、リストおよび階局内の隣接する芁玠を参照できたす。 オブゞェクトがむンスタンス化されるず、次のようなものが埗られたす。







このデヌタ構造は、単玔な二重リンクリストに䌌おいたすが、ON時間に必芁な階局を収集し、必芁に応じおO1時間に階局を収集し、特定の芁玠を削陀するか、リストに新しい芁玠を挿入するこずで倉曎できたす任意のアむテム。 この構造の芁玠ぞのアクセスは、リンクをクリックするこずで達成される、深さのツリヌトラバヌサルに察応するシヌケンシャルにのみ興味がありたす。 赀い矢印に沿った移動は、盎線での移動に察応し、青い矢印に沿った移動は、反察方向ぞの移動に察応したす。



内郚のDiagramObjectリストに新しいオブゞェクトを远加するには、addToQueueDiagramObject subObjメ゜ッドを䜿甚したす。

 if (last!=null)) { last.next = subObj; subObj.previous = last; } else { first = subObj; subObj.previous = null; } subObj.next = null; subObj.parent = this; last = subObj;
      
      







目的の画像を収集するには、適切な数の適切な匕き出しをむンスタンス化し、それらを適切な順序でキュヌに結合するだけです。 この䟋では、この䜜業のほずんどはUseCaseDiagramクラスのコンストラクタヌで発生したす。

 DiagramActor a1 = new DiagramActor(70, 150, "Customer"); addToQueue(a1); DiagramActor a2 = new DiagramActor(50, 350, "NFRC Customer"); addToQueue(a2); DiagramActor a3 = new DiagramActor(600, 50, "Bank Employee"); addToQueue(a3); 
 DiagramUseCase uc1 = new DiagramUseCase(250, 50, "Open account"); addToQueue(uc1); DiagramUseCase uc2 = new DiagramUseCase(250, 150, "Deposit funds"); addToQueue(uc2); 
 addToQueue(new DiagramAssociation(a1, uc1)); addToQueue(new DiagramAssociation(a1, uc2)); 
 addToQueue(new DiagramDependency(uc2, uc5, DependencyStereotype.EXTEND)); addToQueue(new DiagramDependency(uc2, uc6, DependencyStereotype.INCLUDE)); 
 addToQueue(new DiagramGeneralization(a2, a1));
      
      







もちろん、実際にはこれを行うべきではありたせん。描画オブゞェクトを䜜成するプロセスを「コヌドにステッチ」する代わりに、システムのデヌタモデルをルヌトクラスのコンストラクタヌに枡す必芁がありたす。 既にサむクルでこのモデルのオブゞェクトをバむパスしお、匕き出しを䜜成したす。 たずえば、珟圚のダむアグラム「UMLドキュメントモデル」の「ロヌル」に察応に関連付けられたActorクラスの各むンスタンスに察しお、DiagramActorレンダリングクラスのオブゞェクトをむンスタンス化する必芁がありたす。



匕き出しに察応するモデルオブゞェクトぞのリンクを保存するず䟿利です。 これらのリンクをレンダラヌのデザむナヌのパラメヌタヌの圢匏で盎接枡すのが最も䟿利です。 この䟋では、オブゞェクトの䞖界座暙ず、名前やステレオタむプなどのパラメヌタヌが、オブゞェクトの代わりに送信されたす。



䞖界座暙ず画面座暙



「䞖界座暙」ずいう甚語を䜿甚したら、すぐにそれが䜕であるかを明確にする必芁がありたす。 私たちの囜の「䞖界座暙」は、図党䜓が収たる「想像䞊のミリメヌトル玙」䞊の図のオブゞェクトの座暙であり、巊䞊隅に原点があり、スケヌリングは行われたせん。 画像の瞮尺が11で、スクロヌルバヌが最小䜍眮にある堎合、䞖界座暙は画面䞊の座暙ず䞀臎したす。 画面ずは異なり、ワヌルド座暙は敎数型ではありたせんが、浮動小数点倀を取りたす。 これは、画像のピクセル化がスケヌルの増加ずずもに発生しないようにするために必芁です。 たずえば、11のスケヌルでは、䞖界座暙0.3の倀はれロのスクリヌンピクセルず区別できたせんが、1001のスケヌルでは30スクリヌンピクセルになりたす。



チャヌトモデルはズヌムやスクロヌルなどの瞬間的なナヌザヌアクションに䟝存しないため、䞖界座暙でチャヌトモデルを正確に蚈算しお保存するず䟿利です。



䞖界座暙を画面に倉換するために、DiagramObjectクラスには重芁なメ゜ッドscaleX...、scaleY...、および単にscale...が含たれおいたす。 最初の2぀は、スケヌル係数をワヌルド座暙に適甚し、氎平スクロヌルバヌず垂盎スクロヌルバヌのシフトをそれぞれ考慮したす。 最埌の方法であるスケヌル...は、スケヌル係数を䜿甚したすが、シフトは考慮したせん䜍眮ではなくサむズたずえば、長方圢の幅や円の半埄を蚈算するために必芁です。



DiagramObjectの芳点からチャヌトを描画したす。 独立、半独立、䟝存オブゞェクト





チャヌトを描画するには、ルヌトDiagramObjectのdrawグラフィックスキャンバス、ダブルaDX、ダブルaDY、ダブルスケヌルメ゜ッドが呌び出されたす。 そのパラメヌタヌは次のずおりです。



このメ゜ッドは、 テンプレヌトメ゜ッドのデザむンパタヌンを実装し、次のようになりたす。

 this.canvas = canvas; this.scale = scale; dX = aDX; dY = aDY; saveCanvasSetup(); internalDraw(canvas); restoreCanvasSetup(); DiagramObject curObj = first; while (assigned(curObj)) { curObj.draw(canvas, aDX, aDY, scale); curObj = curObj.next; }
      
      







぀たり、描画...メ゜ッド



したがっお、アルゎリズムの䞍倉郚分はdraw...メ゜ッドで実装され、可倉郚分実際の描画は埌継クラスで実装されたす。これがTemplate Methodパタヌンの本質です。



saveCanvasSetupおよびrestoreCanvasSetupメ゜ッドの目的は、描画コンテキストの状態を保存しお、各描画オブゞェクトが「元の」圢匏で取埗できるようにするこずです。 これらの方法が䜿甚されおおらず、描画盞続人の1぀で、むンクの色が赀に倉曎された堎合、埌で描画されるものはすべお赀で描画されたす。 これらのメ゜ッドの実装は、開発環境ず描画゚ンゞンが提䟛する機胜に䟝存したす。 たずえば、DelphiずJava Swingでは、倚くのコンテキストパラメヌタヌを保存する必芁がありたす。たた、HTML Canvas2Dには、コンテキストのすべおの状態を特別なスタックにすぐに保存するための既補のsaveおよびrestoreメ゜ッドがありたす。



DiagramActorクラスでinternalDrawメ゜ッドがどのように芋えるかを瀺したす最初の「単玔な䟋」ず比范しおください

 static final double ACTOR_WIDTH = 25.0; static final double ACTOR_HEIGHT = 35.0; @Override protected void internalDraw(Graphics canvas) { double mX = getmX(); double mY = getmY(); canvas.drawOval(scaleX(mX + 10 - ACTOR_WIDTH / 2), scaleY(mY + 0 - ACTOR_HEIGHT / 2), scale(10), scale(10)); canvas.drawLine(scaleX(mX + 15 - ACTOR_WIDTH / 2), scaleY(mY + 10 - ACTOR_HEIGHT / 2), scaleX(mX + 15 - ACTOR_WIDTH / 2), scaleY(mY + 25 - ACTOR_HEIGHT / 2)); canvas.drawLine(scaleX(mX + 5 - ACTOR_WIDTH / 2), scaleY(mY + 15 - ACTOR_HEIGHT / 2), scaleX(mX + 25 - ACTOR_WIDTH / 2), scaleY(mY + 15 - ACTOR_HEIGHT / 2)); canvas.drawLine(scaleX(mX + 5 - ACTOR_WIDTH / 2), scaleY(mY + 35 - ACTOR_HEIGHT / 2), scaleX(mX + 15 - ACTOR_WIDTH / 2), scaleY(mY + 25 - ACTOR_HEIGHT / 2)); canvas.drawLine(scaleX(mX + 25 - ACTOR_WIDTH / 2), scaleY(mY + 35 - ACTOR_HEIGHT / 2), scaleX(mX + 15 - ACTOR_WIDTH / 2), scaleY(mY + 25 - ACTOR_HEIGHT / 2)); }
      
      







ポむントmX、mYは、オブゞェクトの䞭倮です。 「単玔な䟋」の原点は巊䞊隅にあるため、オブゞェクトの幅ず高さの半分だけシフトする必芁がありたす。 「単玔な䟋」では、画像のスケヌリングずシフトの必芁性を考慮しおいたせんでしたが、メ゜ッドscaleX...、scaleY...、scale...を䜿甚しお䞖界座暙を画面に倉換するずき、これを考慮したす。



DiagramActorおよびDiagramUseCaseオブゞェクトは完党に「独立」しおおり、それらの䜍眮はmXおよびmYフィヌルドに栌玍されおいる内郚状態によっお完党に決定されたす。 同時に、すべおの皮類の接続矢印には独自の状態がありたせん。画面䞊の䜍眮は、接続するオブゞェクトの䜍眮によっお完党に決定され、完党に「独立」ではなく、オブゞェクトの䞭心を結ぶ盎線に沿っお移動したす







たた、個別にオブゞェクトの眲名に泚意する必芁がありたす。 内郚状態では、絶察座暙ではなく、芪の描画オブゞェクトに察するオフセットが保存されるため、「半独立」に動䜜したす。







マりスカヌ゜ルの䞋のオブゞェクトの定矩



図面を扱った埌、どのオブゞェクトがクリックされたかをチャヌトがどのように「理解」するかずいう質問に移りたす。 マりスカヌ゜ルの䞋のオブゞェクトを決定するタスクは、レンダリングタスクず非垞によく䌌おおり、ある意味では察称的です。



たず、特定のチャヌトオブゞェクトごずに、カヌ゜ルがこのオブゞェクト䞊にあるかどうかをマりスカヌ゜ルの座暙によっお決定するメ゜ッドを蚘述するこずは難しくありたせん。



たずえば、DiagramActorの堎合、長方圢の領域に入るこずに぀いお話したす

  protected boolean internalTestHit(double x, double y) { double dX = x - getmX(); double dY = y - getmY(); return dY > -ACTOR_HEIGHT / 2 && dY < ACTOR_HEIGHT / 2 && dX > -ACTOR_WIDTH / 2 && dX < ACTOR_WIDTH / 2; }
      
      





DiagramUseCaseに぀いおは、楕円のように芋える領域に入るこずに぀いお話しおいたす

  protected boolean internalTestHit(double x, double y) { double dX = 2 * getScale() * (x - getmX()) / (width + 2 * MARGIN / getScale()); double dY = 2 * (y - getmY()) / HEIGHT; return dX * dX + dY * dY <= 1; }
      
      





これで、カヌ゜ルが眮かれおいるオブゞェクトを特定したい堎合、シヌケンシャル怜玢によっお各チャヌトオブゞェクトのinternalTestHitを呌び出すこずができ、trueを返す最初のオブゞェクトが目的のオブゞェクトになりたす。 これは、レンダリングの逆の順序でのみ行う必芁がありたすデヌタ構造を瀺す図の青い矢印に沿った移動 マりスカヌ゜ルが耇数のオブゞェクトが亀差する領域にある堎合、カヌ゜ルが他のオブゞェクトよりも遅く描画されたオブゞェクト、぀たり芖芚的に「他のオブゞェクトの䞊」にヒットするのは逆順の怜玢です。







別のDiagramObjectテンプレヌトメ゜ッドでの実装方法は次のずおりです。

 public final DiagramObject testHit(int x, int y) { DiagramObject result; DiagramObject curObj = last; while (assigned(curObj)) { result = curObj.testHit(x, y); if (assigned(result)) return result; curObj = curObj.previous; } if (internalTestHit(x / scale + dX, y / scale + dY)) result = this; else { result = null; } return result; }
      
      







DiagramPanelオブゞェクトは、ルヌトレンダリングオブゞェクトのtestHitメ゜ッドを呌び出したす。 実行䞭に、描画方向ずは反察の方向に深さでレンダリングツリヌを暪断する再垰が行われたす。 最初に芋぀かったオブゞェクトが返されたす。これは、マりスカヌ゜ルの䞋にある、ナヌザヌの芖点から芋た「トップ」オブゞェクトになりたす。



マりスカヌ゜ルの䞋の珟圚のコンテキストの定矩



マりスカヌ゜ルの䞋のオブゞェクトは、より倧きなオブゞェクトの䞍可欠な郚分にしかならず、独立した意味を持぀こずはできたせん。 オブゞェクトに察しお䜕らかの操䜜を実行し、その郚分をマりスでクリックする堎合は、芪オブゞェクトに察しお操䜜を実行する必芁がありたす。 耇合パタヌンの䜿甚に関連する手法である委任の助けを借りお、コンテキストを正しく衚瀺するこずができたすこのテヌマに関する本デザむンパタヌンを参照しおください。 この䟋では、委任を䜿甚しおオブゞェクトのツヌルチップを取埗したす。たずえば、ナヌザヌがアクタヌの眲名の䞊にマりスカヌ゜ルを移動するず、カヌ゜ルがアクタヌ自䜓に眮かれたずきず同じヒントを取埗したす。



考え方は非垞に単玔です。DiagramObjectクラスのgetHintメ゜ッドは次のこずを行いたす。internalGetHintメ゜ッドの独自の実装がプロンプト文字列を返すこずができる堎合、戻りたす。 それができない堎合、レンダリング階局内の芪にアクセスしお、getHintメ゜ッドを実行できるかどうかを確認したす。 それが「行われない」堎合、「責任の移転」は、非垞に根本的なオブゞェクト匕き出したで続きたす。 委任メカニズムに加えお、 テンプレヌトメ゜ッドパタヌンを再床適甚したす。

  public String getHint() { StringBuilder hintStr = new StringBuilder(); if (internalGetHint(hintStr)) return hintStr.toString(); else if (assigned(parent)) return parent.getHint(); else { return ""; } } protected boolean internalGetHint(StringBuilder hintStr) { return false; }
      
      





ヘルパヌメ゜ッドDiagramObject



DiagramObjectの盞続人は、次のメ゜ッドをオヌバヌラむドできたす。DiagramPanelクラスでのそれらの䜿甚は、以䞋から明らかになりたす。





図面の遞択



最埌に、DiagramObjectレベルで実装される別の重芁な機胜は、その子孫で再定矩できたすが、遞択、぀たりグラフィックラベルを描画したす。これにより、ナヌザヌはオブゞェクトが遞択された状態にあるこずを理解できたす。 デフォルトでは、これらはオブゞェクトの角にある4぀の青い正方圢のドットです。



 private static final int L = 4; protected void internalDrawSelection(Graphics canvas, int dX, int dY) { canvas.setColor(Color.BLUE); canvas.setXORMode(Color.WHITE); canvas.fillRect(minX() + dX - L, minY() + dY - L, L, L); canvas.fillRect(maxX() + dX, minY() + dY - L, L, L); canvas.fillRect(minX() + dX - L, maxY() + dY, L, L); canvas.fillRect(maxX() + dX, maxY() + dY, L, L); canvas.setPaintMode(); }
      
      





敎数したがっお画面座暙パラメヌタヌdX、dY、およびsetXORModeの呌び出しに泚意しおください。これは、レンダリングコンテキストを「XORモヌド」に切り替えたす。このモヌドでは、以前に描画した画像を消去するには、再描画するだけで十分です。これは、チャヌトオブゞェクトにドラッグアンドドロップを実装するために必芁です。簡単にするために、画像自䜓ではなく、マりスで「ドラッグ」しお、画像を新しい堎所に転送したす。画面内のオブゞェクトのオフセットは、dX、dYパラメヌタ開始䜍眮を基準ずした座暙





システムのこの動䜜が適切でない堎合、DiagramObjectクラスの継承者のinternalDrawSelectionメ゜ッドをオヌバヌラむドしお、遞択ずしおより耇雑なものを描画したすそしおドラッグアンドドロップで移動したす。



* * *



DrawObjectクラスに぀いおは以䞊です。この蚘事の第2郚では、DiagramPanelクラスの構築に぀いお説明したす。このクラスは、マりスむベントの凊理、スケヌリング、パン、オブゞェクトの遞択、およびドラッグアンドドロップを担圓したす。この䟋の完党な゜ヌスコヌドは、https//github.com/inponomarev/graphexampleから入手でき、Mavenを䜿甚しおコンパむルできたす。



All Articles