javafx開發exe程序內嵌web項目

目的

開發打包一個exe軟件,用於展現web項目內容。[僅供學習使用]

使用 javaFX | BorderPane 佈局css

1.top部分 自定義窗口頭部 ( icon, 標題,返回按鈕,清理按鈕,縮小按鈕,放大按鈕,關閉按鈕)java

2.center部分 嵌入chrome內核瀏覽器(jxbrowser),用於展現項目內容linux

預覽

開發環境

  1. 32位是爲了打出32位的程序和安裝包,使用32位是爲了讓32位和64位系統均可使用。
  2. jre1.8是javafx的運行時環境,爲了可以讓程序在其餘沒有安裝jdk的電腦上安裝運行,須要把32位的jre環境打包進程序中.
  3. 使用jxbrowser不是用自帶webview的緣由在於 webview卡頓嚴重且渲染頁面會形成樣式錯亂.
  1. jdk1.8
  2. exe4j 5.0.1 (32位)
  3. inno setup 5.6.1 (32位)
  4. jre1.8的運行文件(32位)
  5. jxbrowser-6.22.1.jar 和 jxbrowser-win32-6.22.1.jar (須要破解才能使用【僅供學習】, 破解步驟以下)web

    1. teamdev.licenses (放在打jar包生成的 META-INF 中)chrome

      Product: JxBrowser
      Version: 6.x
      Licensed to: Kagura.me
      License type: Enterprise
      License info: JxBrowser License
      Expiration date: 01-01-9999
      Support expiration date: NO SUPPORT
      Generation date: 01-01-1970
      Platforms: win32/x86;win32/x64;mac/x86;mac/x64;linux/x86;linux/x64
      Company name: TeamDev Ltd.
      SigB: 1
      SigA: 1
    2. 代碼中(靜態代碼塊, 必須比其餘代碼先運行)瀏覽器

      static {
       try {
       Field e = bb.class.getDeclaredField("e");
       e.setAccessible(true);
       Field f = bb.class.getDeclaredField("f");
       f.setAccessible(true);
       Field modifersField = Field.class.getDeclaredField("modifiers");
       modifersField.setAccessible(true);
       modifersField.setInt(e, e.getModifiers() & ~Modifier.FINAL);
       modifersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
       e.set(null, new BigInteger("1"));
       f.set(null, new BigInteger("1"));
       modifersField.setAccessible(false);
       } catch (Exception e1) {
       e1.printStackTrace();
       }
      }

開發思路

  1. 利用javaFX開發外部窗口,而後嵌入chrome瀏覽器
  2. 將開發出的程序打成jar包
  3. 利用exe4j將jar包打成exe啓動程序
  4. 在用inno setup將exe程序封裝成一個程序安裝包

代碼

  1. 若是沒有項目地址,可用browser.loadHTML("測試頁面"), 加載dom節點進行頁面渲染;
  2. 若是有項目地址,可用browser.loadURL(testUrl); 直接獲取項目頁面

    或者可用第三方網站地址 browser.loadURL(「https://www.baidu.com」)獲取內容查看緩存

其餘細節看代碼註釋app

package com.yemin.inspect;

import com.teamdev.jxbrowser.chromium.*;
import com.teamdev.jxbrowser.chromium.javafx.BrowserView;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.Modality;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.UUID;


public class Main extends Application {
    //破解代碼,用於破解jxbrowser包(僅供學習使用)
    static {
        try {
            Field e = bb.class.getDeclaredField("e");
            e.setAccessible(true);
            Field f = bb.class.getDeclaredField("f");
            f.setAccessible(true);
            Field modifersField = Field.class.getDeclaredField("modifiers");
            modifersField.setAccessible(true);
            modifersField.setInt(e, e.getModifiers() & ~Modifier.FINAL);
            modifersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
            e.set(null, new BigInteger("1"));
            f.set(null, new BigInteger("1"));
            modifersField.setAccessible(false);
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }

    private final boolean production = false;//是否生產
    private final String url = "<h1>hello world</h1>";//生產地址
    private HashMap<String, String> testEnvironmentsUrls;//測試地址

    private double x = 0.00;
    private double y = 0.00;
    private double width = 0.00;
    private double height = 0.00;
    private boolean isMax = false;
    private boolean isRight;// 是否處於右邊界調整窗口狀態
    private boolean isBottomRight;// 是否處於右下角調整窗口狀態
    private boolean isBottom;// 是否處於下邊界調整窗口狀態
    private double RESIZE_WIDTH = 5.00;
    private double MIN_WIDTH = 400.00;
    private double MIN_HEIGHT = 300.00;
    private double xOffset = 0, yOffset = 0;//自定義dialog移動橫縱座標

     /**
     * testEnvironmentsUrls 是 production變量爲false(測試環境下),點擊頭部icon可
     * 彈出窗口進行選擇訪問環境的地址
     * @throws Exception
     */
    @Override
    public void init() throws Exception {
        super.init();
        testEnvironmentsUrls = new HashMap<String, String>();
        testEnvironmentsUrls.put("測試1", "<h1>測試1</h1>");
        testEnvironmentsUrls.put("測試2", "<h1>測試2</h1>");
    }
    
     /**
     * 1. Stage 是程序窗口 ---》 舞臺
     * 2. Scene 是程序頁面 ----》 場景 (可舞臺固定只切換場景)
     * 3. 其餘的按鈕之類的東西是 放在scene上, 而後scene在放入stage
     * 佈局指的是在scene內佈局(常見佈局請查閱相關資料)
     * 4 .根據須要對窗口,頁面,元素添加對應的監聽代碼
     * @param primaryStage
     * @throws Exception
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        System.out.println("當前訪問頁面: " + url);
        primaryStage.initStyle(StageStyle.TRANSPARENT);
        BorderPane root = new BorderPane();
        //-------設置頭部bar--------
        GridPane gpTitle = new GridPane();
        gpTitle.setAlignment(Pos.CENTER_LEFT);
        gpTitle.setPadding(new Insets(8));
        String title = "javaFX開發打包測試";
        Label lbTitle = new Label(title);
        lbTitle.setTextFill(Color.web("#ccc"));
        lbTitle.setFont(new Font("Arial", 14));

        ImageView imageView = new ImageView("/img/icon.png");
        imageView.setFitWidth(20);
        imageView.setFitHeight(20);
        lbTitle.setGraphic(imageView);
        Button btnMin = new Button();
        btnMin.setId("minButton");
        btnMin.setPrefSize(20, 20);

        Button btnMax = new Button();
        btnMax.setId("maxButton");
        btnMax.setPrefSize(20, 20);

        Button btnClose = new Button();
        btnClose.setId("closeButton");
        btnClose.setPrefSize(20, 20);

        Button btnBack = new Button();
        btnBack.setId("backButton");
        btnBack.setPrefSize(20, 20);

        Button btnClean = new Button();
        btnClean.setId("btnClean");
        btnClean.setPrefSize(18, 18);

        gpTitle.add(lbTitle, 0, 0);
        gpTitle.add(btnBack, 1, 0);
        gpTitle.add(btnClean, 2, 0);
        gpTitle.add(btnMin, 3, 0);
        gpTitle.add(btnMax, 4, 0);
        gpTitle.add(btnClose, 5, 0);

        gpTitle.setStyle("-fx-background-color: black;");
        gpTitle.setPrefHeight(20);
        gpTitle.setMaxHeight(20);
        GridPane.setHgrow(lbTitle, Priority.ALWAYS);
        GridPane.setMargin(btnBack, new Insets(0, 6, 0, 0));
        GridPane.setMargin(btnClean, new Insets(0, 6, 0, 0));
        GridPane.setMargin(btnMin, new Insets(0, 6, 0, 0));
        GridPane.setMargin(btnMax, new Insets(0, 6, 0, 0));
        GridPane.setMargin(btnClose, new Insets(0, 6, 0, 0));

        root.setTop(gpTitle);

        //-------設置內容部分--------
        BrowserContext context = new BrowserContext(new BrowserContextParams("/tmp/" + UUID.randomUUID().toString()));
        Browser browser = new Browser(BrowserType.LIGHTWEIGHT, context);
        BrowserView browserView = new BrowserView(browser);
        if (production) {
            browser.loadHTML(url);
        } else {
            String testUrl = testEnvironmentsUrls.get("測試1");
            browser.loadHTML(testUrl);
        }
        root.setCenter(browserView);
        root.getCenter().setStyle("-fx-background-color: white;visibility: visible");
        //-----------按鈕事件監聽--------------
        btnMin.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                primaryStage.setIconified(true);
            }
        });
        btnMax.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                Rectangle2D rectangle2d = Screen.getPrimary().getVisualBounds();
                isMax = !isMax;
                if (isMax) {
                    // 最大化
                    primaryStage.setX(rectangle2d.getMinX());
                    primaryStage.setY(rectangle2d.getMinY());
                    primaryStage.setWidth(rectangle2d.getWidth());
                    primaryStage.setHeight(rectangle2d.getHeight());
                } else {
                    // 縮放回原來的大小
                    primaryStage.setX(x);
                    primaryStage.setY(y);
                    primaryStage.setWidth(width);
                    primaryStage.setHeight(height);
                }
            }
        });
        btnClose.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                clearData(browser);
                browser.stop();
                primaryStage.close();
                Platform.exit();
                System.exit(0);
            }
        });
        btnBack.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                String nowWebViewUrl = browser.getURL();
                if (nowWebViewUrl.contains("menunav.jsp")) {
                    browser.executeJavaScript("parent.document.getElementById('main-iframe').contentWindow.history.go(-1);");
                }
            }
        });
        btnClean.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                System.out.println("清除緩存,跳轉頁面: " + browser.getURL());
                redirectUrl(browser, browser.getURL());
            }
        });
        //窗口大小位置事件監聽
        primaryStage.xProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                if (newValue != null && !isMax) {
                    x = newValue.doubleValue();
                }
            }
        });
        primaryStage.yProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                if (newValue != null && !isMax) {
                    y = newValue.doubleValue();
                }
            }
        });
        primaryStage.widthProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                if (newValue != null && !isMax) {
                    width = newValue.doubleValue();
                }
            }
        });
        primaryStage.heightProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                if (newValue != null && !isMax) {
                    height = newValue.doubleValue();
                }
            }
        });
        //鼠標移動事件監聽
        root.setOnMouseMoved((MouseEvent event) -> {
            event.consume();
            double x = event.getSceneX();
            double y = event.getSceneY();
            double width = primaryStage.getWidth();
            double height = primaryStage.getHeight();
            // 鼠標光標初始爲默認類型,若未進入調整窗口狀態,保持默認類型
            Cursor cursorType = Cursor.DEFAULT;
            // 先將全部調整窗口狀態重置
            isRight = isBottomRight = isBottom = false;
            if (y >= height - RESIZE_WIDTH) {
                if (x <= RESIZE_WIDTH) {
                    // 左下角調整窗口狀態
                    //不處理
                } else if (x >= width - RESIZE_WIDTH) {
                    // 右下角調整窗口狀態
                    isBottomRight = true;
                    cursorType = Cursor.SE_RESIZE;
                } else {
                    // 下邊界調整窗口狀態
                    isBottom = true;
                    cursorType = Cursor.S_RESIZE;
                }
            } else if (x >= width - RESIZE_WIDTH) {// 右邊界調整窗口狀態
                isRight = true;
                cursorType = Cursor.E_RESIZE;
            }
            // 最後改變鼠標光標
            root.setCursor(cursorType);
        });
        //鼠標拖拽事件
        root.setOnMouseDragged((MouseEvent event) -> {
            //根據鼠標的橫縱座標移動dialog位置
            event.consume();
            if (yOffset != 0) {
                primaryStage.setX(event.getScreenX() - xOffset);
                if (event.getScreenY() - yOffset < 0) {
                    primaryStage.setY(0);
                } else {
                    primaryStage.setY(event.getScreenY() - yOffset);
                }
            }
            double x = event.getSceneX();
            double y = event.getSceneY();
            // 保存窗口改變後的x、y座標和寬度、高度,用於預判是否會小於最小寬度、最小高度
            double nextX = primaryStage.getX();
            double nextY = primaryStage.getY();
            double nextWidth = primaryStage.getWidth();
            double nextHeight = primaryStage.getHeight();
            // 全部右邊調整窗口狀態
            if (isRight || isBottomRight) {
                nextWidth = x;
            }
            // 全部下邊調整窗口狀態
            if (isBottomRight || isBottom) {
                nextHeight = y;
            }
            // 若是窗口改變後的寬度小於最小寬度,則寬度調整到最小寬度
            if (nextWidth <= MIN_WIDTH) {
                nextWidth = MIN_WIDTH;
            }
            // 若是窗口改變後的高度小於最小高度,則高度調整到最小高度
            if (nextHeight <= MIN_HEIGHT) {
                nextHeight = MIN_HEIGHT;
            }
            // 最後統一改變窗口的x、y座標和寬度、高度,能夠防止刷新頻繁出現的屏閃狀況
            primaryStage.setX(nextX);
            primaryStage.setY(nextY);
            primaryStage.setWidth(nextWidth);
            primaryStage.setHeight(nextHeight);
        });
        //鼠標點擊獲取橫縱座標
        root.setOnMousePressed(event -> {
            event.consume();
            xOffset = event.getSceneX();
            if (event.getSceneY() > 46) {
                yOffset = 0;
            } else {
                yOffset = event.getSceneY();
            }
        });
        //非生產可選擇環境
        if (!production) {
            lbTitle.setOnMouseClicked(event -> {
                chooseEnvironment(browser);
            });
        }
        //------------啓動構建---------------
        Scene scene = new Scene(root);
        scene.setUserAgentStylesheet("/css/mainStage.css");
        primaryStage.setScene(scene);
        primaryStage.setTitle(title);
        primaryStage.getIcons().add(new Image("/img/icon.png"));
        primaryStage.show();
        btnMax.fire();//觸發最大化按鈕
    }

    public void chooseEnvironment(Browser browser) {
        Stage window = new Stage();
        window.setTitle("選擇環境");
        window.initModality(Modality.APPLICATION_MODAL);
        window.setHeight(150);
        window.setWidth(300);
        Label label = new Label("請選擇環境");
        VBox layout = new VBox(10);
        layout.getChildren().add(label);
        for (String key : testEnvironmentsUrls.keySet()) {
            Button button = new Button(key);
            button.setOnAction(e -> {
                redirectUrl(browser, testEnvironmentsUrls.get(key));
                window.close();
            });
            layout.getChildren().add(button);
        }
        layout.setAlignment(Pos.CENTER);
        Scene scene = new Scene(layout);
        window.setScene(scene);
        window.showAndWait();
    }

    private void redirectUrl(Browser browser, String url) {
        System.out.println("切換地址: " + url);
        clearData(browser);
        browser.loadHTML(url);
    }

    private void clearData(Browser browser){
        browser.getCacheStorage().clearCache();
        browser.getCookieStorage().deleteAll();
        browser.getLocalWebStorage().clear();
        browser.getSessionWebStorage().clear();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

項目編譯成JAR包

1.引入第三方包 jxbrowser-6.22.1.jar 和 jxbrowser-win32-6.22.1.jar (將第三方包的export勾選起來), 這樣能夠把第三方的包最後全都打包打主jar包中
imagedom

2.artifacts構建jar包
imagejsp

3.build --> build artifacts --> rebuild
image

JAR包 轉 EXE (6,7兩步是重點)

exe4j 註冊碼: A-XVK258563F-1p4lv7mg7sav

1.change license 弄一個能夠用的註冊碼
image

2.咱們是jar包轉exe,選擇第二個
image

3.配置exe的名字和輸出到哪一個文件夾
image

4.Icon File 能夠配置應用圖標
image

5.看你要打幾位的exe文件本身按須要選擇
image

6.這一步是重點,你要把以前的打出的jar包放進來, 而後選擇main class
image

7.配置程序的運行時jre環境的位置. 這邊我只配置爲相鄰的jre文件夾,因此編譯輸出的exe文件要與jre文件相鄰,才能啓動運行。若是須要拿到別的電腦運行,要把jre和exe都複製過去
image

8.其餘的就順序一個個走下去,基本都是默認的就能夠了

EXE 打成安裝包(第4步是重點)

1.啓動inno setup ,新建一個文件
image

2.安裝包名稱版本之類的信息,本身根據要求輸入
image

3.按默認便可
image

4.這步是重點!! 把以前編譯的exe文件放到主執行文件中, 而後jre的文件夾放到,下面的其餘應用程序文件文件中
image

5.默認或根據需求選擇
image

6.輸出文件夾,文件名稱,安裝包圖標
image

7.其餘默認

8.最後會生成一個編譯腳本, 有編譯按鈕(構建安裝包)和啓動按鈕(安裝程序)
image

; 腳本由 Inno Setup 腳本嚮導 生成!
; 有關建立 Inno Setup 腳本文件的詳細資料請查閱幫助文檔!
​
#define MyAppName "這個是app名稱"
#define MyAppVersion "app版本"
#define MyAppPublisher "app發佈者"
#define MyAppURL "app url地址"
#define MyAppExeName "app執行名稱.exe"
​
[Setup]
; 注: AppId的值爲單獨標識該應用程序。
; 不要爲其餘安裝程序使用相同的AppId值。
; (生成新的GUID,點擊 工具|在IDE中生成GUID。)
AppId={{0B52F1C0-A71F-48DE-8C2E-940B29412328}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={pf}{#MyAppName}
DisableProgramGroupPage=yes
OutputDir=C:UsersAdministratorDesktopxxSystem安裝包名稱setup
OutputBaseFilename=安裝包名稱setup
SetupIconFile=C:UsersAdministratorDesktopxxSystemprojecticon.ico
Compression=lzma
SolidCompression=yes
​
[Languages]
Name: "chinesesimp"; MessagesFile: "compiler:Default.isl"
​
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkablealone; OnlyBelowVersion:0,6.3
​
[Files]
Source: "C:UsersAdministratorDesktopxxSystemprojectoutputapp啓動程序名稱.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:UsersAdministratorDesktopxxSystemprojectjre*"; DestDir: "{app}jre"; Flags: ignoreversion recursesubdirs createallsubdirs
; 注意: 不要在任何共享系統文件上使用「Flags: ignoreversion」
​
[Icons]
Name: "{commonprograms}{#MyAppName}"; Filename: "{app}{#MyAppExeName}"
Name: "{commondesktop}{#MyAppName}"; Filename: "{app}{#MyAppExeName}"; Tasks: desktopicon
​
[Run]
Filename: "{app}{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

總結

1.開發中的重點在於佈局和嵌入瀏覽器

2.後面編譯的重點在於jre環境程序,必定要打進去,否則不帶jre的安裝包,別人安裝了也沒法使用

相關文章
相關標籤/搜索