Spring Groovyコンテキストを使用して、構成可能なインタラクティブなグラフィカルUIを作成する

90年代後半、彼は接触電気ネットワークの開発と電源ユニット(牽引変電所)の運用に携わる1つの組織で働き、さらにいくつかのコントロールセンターからの特殊なハードウェアとソフトウェアの複合体を使用してこの経済を監視および制御しました。 複合体は、古代ソビエト鉄の自動プロセス制御システムの制御下で機能しました。 その後、タスクは、ウィンドウ全体でこの複合体を翻訳することでした。これには、ネットワーク全体およびネットワークで発生するイベントと変電所をマップ形式で表示するグラフィカルインターフェイスの開発が含まれます。 もちろん、それはそれらを管理する機会を提供します。



タスクの説明



技術的には、タスクはネットワーク(IP)を介してデータ(パケットメッセージ)を取得し、解析(変電所のデータとその変更を取得)し、最後にこれらすべてをUIクライアントに表示(マップ/ダイアグラムで変更を表示、たとえば、要素の色を変更して点滅させます)。 各変電所には特定のセンサーとカウンターのセットがあり、そのデータはメッセージパケットに含まれていました。 必要でした。 地図上で変電所を選択し、オペレータがコマンドを送信できるインタラクティブな制御要素を備えた回路図形式で表示する可能性を実現します。



実装



ご存知のように、グラフィック要素は非常に具体的であるため、標準のものを使用することはできませんでした。さらに、実際には、アニメーションを実装する必要がありました。 そして、変更が対応するフレームに描画され、特定の頻度で変更があった場合、ダブルバッファリングを使用してソリューションが選択されました。 最初の実装はDelphiで行われました。 しかし、運用の過程で、機器とネットワークの構成の表示を変更することがしばしば必要であるという事実に直面しました。 その結果、ソースコードへの介入が必要になるたびに(Delphiで何らかのコンフィギュレータを書くのが面倒でした)、どういうわけかうまく動作しませんでした)。 そして1999年に、私は最初にJavaを知りました。 その後、シンプルなスクリプト言語(Groovyに似ていたので、それについて聞いたことがありませんでした。Groovyが2003年に登場したため)を使用して、スキームを説明するリフレクションテクノロジーに夢中になりました。 Groovyと同様に、Javaオブジェクトを使用します。



以下は、Tp、Bus、Lab、Led、Boxが、タスクで上記の機能を実装できるグラフィックプリミティブのJavaラッパークラスにすぎないスクリプトのスニペットです。



Root.txt

setFormFactor:4 4 setResolution:700 800 setScale:0.45 setScaleStep:0.1 define:"" "contr.txt" define:"1" "src/1.txt" define:"2" "src/2.txt" define:"3" "src/3.txt" define:"1" "dev/1.txt" define:"2" "dev/2.txt" define:"3" "dev/3.txt" define:"4" "dev/4.txt" define:"" "sw/0.txt" define:"1" "sw/1.txt" define:"2" "sw/2.txt" define:"3" "sw/3.txt" define:"4" "sw/4.txt" define:"5" "sw/5.txt" define:"6" "sw/6.txt" add:new Tp 31 { busWidth:7 setColor:col.red col.green add:new Bus 4 0 0 2 523 106 627 160 add:new Bus 5 0 0 2 636 164 683 190 add:new Bus 6 0 0 2 689 195 741 243 add:new Bus 29 0 0 2 730 268 650 268 setColor:col.black col add:new Led "" 627 157 8 add:new Led "" 682 187 8 add:new Led "" 739 239 8 add:new Led "" 734 268 8 setFontSize:21 add:new Lab "5311" 590 113 add:new Lab "5312" 670 159 add:new Lab "5313" 714 187 add:new Lab "5314" 660 280 ico:655 123 box { setFontSize:24 add:new Lab "(1802)" 70 5 recall:"" setFontSize:17 setColor:col.red col.black add:new Lab 18 400 200 recall:"1" setColor:col.white col add:new Box "" 10 50 10 10 { setColor:col.black col setFontSize:12 ins:new Lab ".5 -1808 116" 5 0 } recall:"2" setColor:col.white col add:new Box "" 130 50 10 10 { setColor:col.black col ins:new Lab ".6 -1808 206" 5 0 } busWidth:10 setColor:col.blue col add:new Bus "" 0 0 2 30 180 270 180 recall:"1" recall:"2" recall:"" recall:"1" recall:"2" recall:"3" recall:"4" moveX:74 busWidth:10 setColor:col.blue col add:new Bus "" 0 0 2 40 300 PX 300 } }
      
      





たとえば、 リコールメソッドは、 定義する最初の引数(Groovyの評価の類似物)を参照しています。 そこで何が起こるか見てみましょう:



src / 1.txt

 setColor:col.blue col busWidth:5 add:new Bus "" 65 60 2 0 0 0 120 { setColor:col.pink col add:new Box 88 -45 40 95 40 { setColor:col.red col.green add:new Led 90 5 5 19 add:new Led 87 33 5 19 add:new Box 86 66 8 23 23 { add:new Popup { setFontSize:17 add:new Btn 86 23 0 { add:new Act ON 131 setColor:col.black col ins:new Lab "" 5 5 } add:new Btn 86 23 34 { add:new Act OFF 131 setColor:col col.red add:new Box 131 8 8 15 15 { add:new Act TOG "" } setColor:col.black col ins:new Lab "" 25 5 } } } setFontSize:12 add:new Lab "" 5 24 add:new Lab "" 30 24 } }
      
      





src / 2.txt

 setColor:col.blue col busWidth:5 add:new Bus "" 180 60 2 0 0 0 120 { setColor:col.pink col add:new Box 94 -45 40 95 40 { setColor:col.red col.green add:new Led 96 5 5 19 add:new Box 92 66 8 23 23 { add:new Popup { setFontSize:17 add:new Btn 92 23 0 { add:new Act ON 132 setColor:col.black col ins:new Lab "" 5 5 } add:new Btn 92 23 34 { add:new Act OFF 132 setColor:col col.red add:new Box 132 8 8 15 15 { add:new Act TOG "" } setColor:col.black col ins:new Lab "" 25 5 } } } setFontSize:12 add:new Lab "" 5 24 } }
      
      





これらのファイルは、最上位の2つのデバイス(図1)を記述しており、ご覧のとおり、コードの再現性がある程度あります。 したがって、テンプレートはすべての複雑なユニット/デバイスに対して定義され、再利用されます。



画像

1.スクリプトフラグメントの結果



画像

2.連絡先ネットワークと変電所の完全な地図



この形式のアプリケーションはまだ使用中です(この間、バックエンドのいくつかの世代が変更されました!)そして顧客は完全に満足していますが、99年には既存の「自転車」を取り除くという考えをいつまでも忘れませんでしたまだされていません。 そして今、Spring 4の登場と、Groovyの次のコンテキストで、マニュアルを吸ってしまったので、すでにどこかでそれを見たように思えました))(上記参照)。 Spring Boot、Groovy-config、およびJavaFxが新しいGUIクライアントの新しいスタックであることが決定されました...



新しいソリューションのアーキテクチャを見てみましょう。 そして、モデルから始めましょう。これは、javafxグラフィックスプリミティブの抽象ラッパーであり、本質的にはアプリケーションのコアです。 多数のクラスとインターフェースが含まれています。 抽象クラスUnitは、マップと図の作成元となるカスタムグラフィック要素を作成するための基本クラスです。



 public abstract class Unit<T> implements Render { private static Logger log = LoggerFactory.getLogger(Unit.class); private Integer code; private State<T> state; Node node; private List<Unit> lays = new LinkedList<>(); public void init(Map<Integer, State> states) { lays.forEach(e -> e.init(states)); initState(states); } private void initState(Map<Integer, State> states) { if (code != null) { State<T> state = states.get(code); if (state == null) { state = new State<>(code); states.put(code, state); } state.addSlave(this); this.state = state; } } public void setCode(Integer code) { this.code = code; } public Integer getCode() { return code; } public State<T> getState() { return state; } public void setGeom(double[] geom) { node = createShape(geom); initNodeEvents(); } private void initNodeEvents() { if (node != null) { node.setOnMousePressed(this::mousePressed); node.setOnMouseReleased(this::mouseReleased); } } public void setLays(List<Unit> lays) { this.lays = lays; } public void render(Group group) { if (node != null) { group.getChildren().add(node); } render(); lays.forEach(e -> e.render(group)); } protected void mousePressed(MouseEvent e) { log.debug(e.toString()); } protected void mouseReleased(MouseEvent e) { log.debug(e.toString()); } protected abstract Node createShape(double[] geom); }
      
      





画像の変更を管理するRenderインターフェイスを実装します。



 public interface Render { void render(); void next(); }
      
      





render()は状態変更後のレンダリングです。

next()-フレームを変更します。



ユニットには、ネストされたユニットのリストレイのリストがあり、レイヤーごとにシーンを描画および管理できます。 特定のマウスイベントをインターセプトするためのメソッドも含まれています。

この場合、 UnitにはFlashingUnitの子孫があります。

 public abstract class FlashingUnit<T> extends Unit<T> { private static Logger log = LoggerFactory.getLogger(FlashingUnit.class); @Override public void next() { log.debug("{} next()", this); if (getState() != null && node != null) { node.setVisible(!getState().isChanged() || !node.isVisible()); } } }
      
      





、タスクに従ってフレームの変更(変更されたオブジェクトの点滅)を実装するだけです。

例として、テキストグラフィック要素の実装を引用します。



 public class Lab extends FlashingUnit<Integer> { private Color[] color; private String text; private Double size; private Text shape; @Override protected Node createShape(double[] geom) { //Can't set text without graphic context shape = new Text(geom[0], geom[1], ""); if (size != null) { shape.setFont(Font.font(size)); } return shape; } public void setText(String text) { this.text = text; } public void setSize(Double size) { this.size = size; } public void setColor(Color[] color) { this.color = color; } @Override public void render(Group group) { super.render(group); shape.setText(text); } @Override public void render() { final Color[] c = new Color[]{color[0]}; if (getState() != null) { getState().initValue(0); c[0] = color[getState().getValue()]; } shape.setFill(c[0]); } }
      
      





Stateクラスは、グラフィック要素の状態を管理します。



 public class State<T> implements Render { private final int id; private T value; private boolean changed; private List<Render> slaves = new LinkedList<>(); public synchronized void initValue(T value) { if (this.value == null) { this.value = value; } } public State(int id) { this.id = id; } public int getId() { return id; } public synchronized T getValue() { return value; } public synchronized void setValue(T value) { if (!value.equals(this.value)) { this.value = value; changed = true; render(); } } public synchronized boolean isChanged() { return changed; } public synchronized void setChanged(boolean changed) { this.changed = changed; } public void addSlave(Render unit) { slaves.add(unit); } @Override public void render() { Platform.runLater(() -> slaves.forEach(Render::render)); } @Override public void next() { Platform.runLater(() -> slaves.forEach(Render::next)); } }
      
      





、リストスレーブにある共通コードですべての要素の状態とその変更に関する情報を保存し、 Renderインターフェイスを介して下位要素のスレッドセーフな更新を提供します。



ControllerクラスはUnitを拡張し、この場合、一意のid



を持つ別のサブステーションに属するすべてのグラフィック要素とオブジェクトの状態のコンテナです。



 public class Controller extends Unit { private int id; private final Root root; private final Map<Integer, State> states = new HashMap<>(); private Unit scheme; @Autowired public Controller(Root root) { this.root = root; } @PostConstruct void init() { super.init(states); if (scheme != null) { scheme.init(states); } root.addController(this); } public void setId(int id) { this.id = id; } public State getState(int code) { return states.get(code); } public int getId() { return id; } public void setScheme(Unit scheme) { this.scheme = scheme; } public Unit getScheme() { return scheme; } @Override protected Node createShape(double[] geom) { return null; } @Override public void render() { states.values().forEach(Render::render); } @Override public void next() { states.values().forEach(Render::next); } }
      
      





そして最後に、すべてのコントローラー(変電所)のマッピングを含むRootクラス:



 public class Root implements Render { private final Map<Integer, Controller> controllers = new HashMap<>(); void addController(Controller controller) { if (controllers.containsKey(controller.getId())) { throw new DuplicateKeyException(String.format("Controller id %d already exists",controller.getId())); } controllers.put(controller.getId(), controller); } public Controller getController(int id) { return controllers.get(id); } public State getState(int controllerId, int code) { Controller controller = controllers.get(controllerId); if (controller != null) { return controller.getState(code); } return null; } public void render(Group group) { controllers.values().stream().map(Controller::getScheme) .filter(Objects::nonNull).forEach(r -> r.render(group)); } @Override public void render() { controllers.values().forEach(Render::render); } @Override public void next() { controllers.values().forEach(Render::next); } }
      
      





ここで、 コントローラーで@PostConctruct init()およびDIを使用すると、オブジェクトの状態の構成を動的に作成し、構成スクリプト内の不要なコードからユーザーを救うことができます。



それでは、Groovyですべてがどのように見えるかを見てみましょう-config。



root.groovy

 package scheme import com.ldim.granit.ui.model.Controller import com.ldim.granit.ui.model.shape.Box import com.ldim.granit.ui.model.shape.Bus import com.ldim.granit.ui.model.shape.Lab import com.ldim.granit.ui.model.shape.Led import javafx.scene.paint.Color suplyColor = [Color.GREEN, Color.RED] alarmColor = [Color.RED, Color.GREEN] beans { importBeans('classpath:/scheme/srcs.groovy') importBeans('classpath:/scheme/devs.groovy') importBeans('classpath:/scheme/sws.groovy') importBeans('classpath:/scheme/tsns.groovy') importBeans('classpath:/scheme/ctrls.groovy') controller2(Controller) { id = 2 lays = [dev1] } tp9scheme(Bus) { code = 5 color = suplyColor width = 6 geom = [653, 764, 698, 687, 701, 631] lays = [new Bus(code: 29, color: suplyColor, width: 6, geom: [701, 631, 701, 602]), new Bus(code: 6, color: suplyColor, width: 6, geom: [701, 602, 705, 560, 840, 591]), new Led(geom:[4.5, 701, 631, 701, 602]), new Lab(text: '5092', geom: [705, 684]), new Lab(text: '5094', geom: [715, 612]), new Lab(text: '5093', geom: [764, 544]), new Lab(text: '19 ', geom: [595, 630]), new Box(code: 129, color: alarmColor, geom: [598, 593, 10, 10]), new Box(code: 130, color: alarmColor, geom: [598, 603, 10, 10]), new Box(color: [Color.GRAY], geom: [608, 593, 30, 20], lays: [new Lab(size: 11, text: '9', geom: [613, 607])])] } tp9(Controller) { id = 3 lays = [src1, src2, src3, new Bus (color: [Color.BLUE], width: 8, geom: [40, 176, 380, 176]), dev1, dev2, dev3, dev4, new Bus (color: [Color.BLUE], width: 8, geom: [40, 276, 585, 276]), sw0, sw1, sw2, sw3, sw4, sw5, sw6, new Bus (code: 12, color: suplyColor, width: 8, geom: [30, 350, 615, 350]), lbl0, lbl1, lbl2, lbl3, lbl4, tsn1, tsn2, reqBtn, tstBtn, secBtn, okBtn] scheme = tp9scheme } }
      
      





srcs.groovy

 package scheme beanName = 'src1' offsetX = 0 reserved = true devCode = 1 swCode = 11 led1Code = 11 led2Code = 11 evaluate(new File("./src/main/resources/scheme/templates/src.groovy")) beanName = 'src2' offsetX = 120 reserved = false devCode = 1 swCode = 11 led1Code = 11 led2Code = 11 evaluate(new File("./src/main/resources/scheme/templates/src.groovy")) beanName = 'src3' offsetX = 240 reserved = false devCode = 1 swCode = 11 led1Code = 11 led2Code = 11 evaluate(new File("./src/main/resources/scheme/templates/src.groovy"))
      
      





src.groovy

 package scheme.templates import com.ldim.granit.ui.model.shape.Box import com.ldim.granit.ui.model.shape.Bus import com.ldim.granit.ui.model.shape.Lab import com.ldim.granit.ui.model.shape.Led import javafx.scene.paint.Color devX = 40 devY = 100 beans { "${beanName}led1"(Led) { bean -> bean.scope = 'prototype' code = led1Code color = [Color.GREEN, Color.RED] geom = [9, devX + 14 + offsetX, devY + 16] } "${beanName}lbl1"(Lab) { bean -> bean.scope = 'prototype' code = devCode text = '' size = 9 color = [Color.BLACK, Color.BLACK] geom = [devX + 5 + offsetX, devY + 35] } if (reserved) { "${beanName}led2"(Led) { bean -> bean.scope = 'prototype' code = led2Code color = [Color.GREEN, Color.RED] geom = [9, devX + 38 + offsetX, devY + 16] } "${beanName}lbl2"(Lab) { bean -> bean.scope = 'prototype' code = devCode text = '' size = 9 color = [Color.BLACK, Color.BLACK] geom = [devX + 30 + offsetX, devY + 35] } } "${beanName}btn"(Box) { bean -> bean.scope = 'prototype' code = swCode color = [Color.GREEN, Color.RED] press = true geom = [devX + 66 + offsetX, devY + 8, 23, 23] } "${beanName}box"(Box) { bean -> bean.scope = 'prototype' code = devCode color = [Color.GRAY, Color.PINK] geom = [devX + offsetX, devY, 95, 40] lays = reserved ? [ref("${beanName}btn"), ref("${beanName}led1"), ref("${beanName}lbl1"), ref("${beanName}led2"), ref("${beanName}lbl2")] : [ref("${beanName}btn"), ref("${beanName}led1"), ref("${beanName}lbl1")] } "${beanName}"(Bus) { bean -> bean.scope = 'prototype' color = [Color.BLUE] width = 4 geom = [devX + 77 + offsetX, devY - 30, devX + 77 + offsetX, devY + 70] lays = [ref("${beanName}box")] } }
      
      





現在、Groovyの機能を使用して、プロジェクトを再配置することなく、既存のグラフィックプリミティブから機器図のさまざまな構成を作成できます。 そして、既存のコードを効果的に再利用します(ここでは、srcs.groovyおよびsrc.groovyファイルを具体的に示しました)。 プロトタイプのソースコードはこちらです。



おわりに



最新のJavaテクノロジースタックを使用すると、「自転車」を使用せずに、効率的で革新的なものを比較的簡単かつ妥当な時間で実装できます。 GroovyやJavaFx自体さえ知らないうちに、新しいアプリケーションのプロトタイプが数日で「膝」に実装されました。 そして、重要なことは、Java生産開発のための強力でオープンな標準を使用してアプリケーションを作成したことです。



All Articles