javafx开发exe程序内嵌web项目

叶敏

目的

开发打包一个exe软件,用于展示web项目内容。[仅供学习使用]

使用 javaFX | BorderPane 布局

1.top部分 自定义窗口头部 ( icon, 标题,返回按钮,清理按钮,缩小按钮,放大按钮,关闭按钮)

2.center部分 嵌入chrome内核浏览器(jxbrowser),用于展示项目内容

预览

开发环境

  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 (需要破解才能使用【仅供学习】, 破解步骤如下)

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

      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”)获取内容查看

其他细节看代码注释

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包中
image

2.artifacts构建jar包
image

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的安装包,别人安装了也无法使用

阅读 2.1k

java

6 声望
1 粉丝
0 条评论
你知道吗?

java

6 声望
1 粉丝
宣传栏