2024年9月

JDBC

JDBC(Java DataBase Connectivty,Java数据库连接)API,是一种用于执行Sql语句的Java API,可以为关系型数据库提供统一的访问,其由一组Java编写的类和接口组成.

JDBC驱动程序

起初,SUN公司推出JDBC API希望能适用于所有数据库,但实际中是不可能实现的,
各个厂商提供的数据库差异太大,SUN公司于数据库厂商协同之后决定:
由SUN公司提供一套访问数据库的API,各个厂商根据规范提供一套访问自家数据库API的接口,
SUN公司提供的规范API称之为
JDBC
,厂商提供的自家数据库API接口称之为
驱动

JDBC原理

  • 经过SUN公司于各个数据库厂商的协同,JDBC的结构如下图:

JDBC驱动的原理

我们知道了JDBC运行的原理,那么JDBC驱动是怎么运行的呢,我们开始进行探究

  • JDBC驱动的类型


    根据访问数据库数据库技术不同,JDBC驱动程序被访问四类


    • Type1:
      JDBC-ODBC
      桥驱动程序
      • 此类驱动程序由JDBC-ODBC桥金额一个ODBC驱动程序组成
    • Type2:部分Java本地JDBC API驱动程序
      • 此类驱动程序必须在本地计算机上先安装好特点的驱动程序才能进行使用
    • Type3:纯Java的数据库中间件驱动程序(目前主流)
      • Pure Java Driver for Database Middleware
        使用此类驱动时,不需要再本地计算机上安装任何附加软件,但必须再安装数据库管理系统的服务器端加装中间件
        (Middleware)
        ,这个中间件负责所有存取数据库时的必要转换.
        中间件的工作原理是:
        驱动程序将JDBC访问转换为于数据库无关的标准网络协议(通常是HTTP或HTTPS)送出,然后再由中间件服务器将其转换为数据库专用的访问指令,完成对数据库的操作.中间件可以支持对多种数据库的访问.
    • Type4:纯Java的JDBC驱动程序(最理想的驱动程序)
      • Direct-toDatabasePureJavaDriver
        此类驱动程序是之间面向数据库的Java驱动程序,即所谓的”瘦”驱动程序.使用该驱动程序无需安装如何附加软件(包括本地计算机或是数据库服务器端),所有存取的数据库操作都直接由JDBC驱动程序来完成
  • JDBC驱动工作动作


    • 对于第三类Type3驱动程序来说,其是由纯Java语言开发的,此类驱动程序体积最小
      • 下面给出Type3驱动程序的结构图

    - 可见,Type 3,JDBC驱动程序为两层结构,分别为驱动程序客户端和驱动程序服务端,客户端直接于用户交互,**其为用户提供符合JDBC规范的数据库统一编程接口,**客户端将数据请求通过**特定的网络请求**发送值服务器.服务器作为中间件的角色,其负责接收和处理用户的请求,JDBC驱动程序本身不进行直接与数据库的交互,而是借助其他已经实现的驱动,称之为”雇佣”.当然”雇佣”的数量越多,可支持的数据库数量就越多.”雇佣”的数量和成员可以动态的改变,以满足业务的扩展,这也是Type3JDBC性能强大的原因

将JDBC驱动导入idea(以Mysql为例)

  1. 第一步肯定是下载JDBC驱动程序了,各个厂商把特定的驱动程序打包成.jar发布在其官网上大伙可用到官网下载

    下载好后,解压至文件夹备用
  1. 打开idea,在要导入驱动的项目中新建一个名为lib的文件夹(建议命名)

  1. 将下载的zip文件解压,找到其中的mysql-connector-java-8.0.26.jar包,
    将其复制

  1. 复制其中的mysql-connector-java-8.0.18.jar文件,在lib文件夹上右键,粘贴到IDEA中,刚刚新建的lib文件夹里

使导入的驱动生效

  1. 在idea点击File→Project Structure

  1. 在Modules中选择Denpendencies

  1. 点击左侧+号,选择 JARS or directories

  1. 在弹出的窗口中选择刚刚导入 lib 文件夹的驱动,点击Ok

  1. 可以看到Module模块中,多出了一个mysql驱动,选择之后点击Apply,然后Ok

JDBC中的的类及其应用

JDBC API中包含四个常用的接口和一个类,分别是
Connection
接口,
Statement
接口,
PreparedStatement
接口,
ResultSet
接口,
DriverManager
类,jar包中已经包含这些接口的实现类直接使用即可

Statement接口

Statement接口是Java程序执行数据库操作的重要接口,用于已经建立了数据库连接的基础上,向数据库发送要执行的Sql语句

  • 作用
    :执行不带参数的简单Sql语句
  • 主要方法
    • void addBatch(String sql )throws SQLException
      :该方法用于将Sql语句添加到Statement对象的当前命令列表中,
      用于Sql语句的批量处理
    • void clearBatch() throws SQLException
      :立即
      释放
      Statement对象中的命令列表
    • boolean excute(String sql) throws SQLException
      :
      执行指定的Sql语句
      ,成功返回
      true
      否则返回
      false
    • int[] excuteBatch() throws SQLException
      :
      将命令列表中的sql命令提交执行
      ,返回一个int数组表示每个sql语句影响的行数
    • ResultSet excuteQuery(String sql) throws SQLException
      :该方法用于
      执行查询类型(Select类型)的Sql语句,
      返回的查询所获取的结果集
      ResultSet
      对象
    • void close() throws SQLException
      :用于立即释放此Statement对象的数据库和JDBC资源

Connection接口

Connection接口位于java.sql包中,是用于与数据库连接的对象,只有获取了与数据库连接的对象后,才能访问数据库进行操作

  • 作用
    :与数据库进行连接

  • 主要方法


    • Statement createStatement() throws SQLException
      :用于创建一个Statement对象,用于执行Sql语句

    • PreparedStatement prepareStatement(String sql) throws SQLException
      : 创建一个PreparedStatement对象,用于执行预编译的Sql语句

    • CallableStatement prepareCall(String sql)throws SQLException
      :创建一个CallableStatement对象用于执行存储过程或函数

    • void commit() throws SQLException
      :提交当前事务

    • void rollback() throws SQLException
      :回滚事务

    • void close() throws SQLException
      :关闭连接


      在进行数据库连接的时候还要用到DriverManager类中的
      getConnection(url,username,password)
      方法


    E.g:

            String url = "jdbc:mysql://localhost:3306/demo";
            String username = "root";
            String password = "root";
            //建立连接
            Connection connection = DriverManager.getConnection(url, username, password);
            String sql = "SELECT * FROM dept";
            //创建Statement对象执行查询操作
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(sql);
            //处理查询结果
            while (resultSet.next()) {
                String dId = resultSet.getString("d_id");
                String dName = resultSet.getString("d_name");
                String loc = resultSet.getString("loc");
                System.out.println(dId + " " + dName + " " + loc);
            }
            connection.close();
            statement.close();
            resultSet.close();
        }
    

DriverManager类

DriverManager类是JDBC API的核心,该类中包含了与数据库交互操作的方法,类中的方法都由数据库厂商提供

  • 作用:
    管理和协调不同的JDBC驱动程序
  • 主要方法
    • public static Connection getConnection(String url, String user, String password)throws SQLException
      :根据指定的数据库url,用户名以及密码建立数据库连接
    • public static Connection getConnection(String url,Properties info)
      :根据指定的数据库url以及连接属性建立数据库连接
    • public static synchronized void deregisterDriver(Driver driver) throws SQLException
      :从DriverManager管理列表中删除一个驱动,driver参数是要删除的驱动对象

PreparedStatement接口

PreparedStatement接口位于java.servlet包中,其继承了Statement接口

  • 与Statement的区别:
    • 执行速度较快:
      PreparedStatement对象是已经预编译过的,执行速度快于Statement.
      因此若要执行大量的Sql语句时使用PreparedStatement以提高效率
  • 主要方法
    • setXXX()


      此类方法都是设置sql语句中传入的参数里类型


      • void setBinaryStream(int parameterIndex,InputStream x) throws SQLException
        :将二进制流作为sql语句传入的参数,二进制流可用高效地处理图片,音频,视频登媒介,
        parameterIndex
        是参数位置索引
      • void setBoolean(int parameterIndex,boolean x) throws SQLException
        :将boolen作为sql传入的参数类型,parameterIndex为参数位置索引
      • void setByte(int parameterIndex,byte x) throws SQLException
        :将byte作为sql传入的参数类型
      • void setDate(int parameterIndex,Date x) throws SQLException
        : 将java.sql.Date值x做为SQL语句中的参数值
      • void setDouble(int parameterIndex,double x)
        :将double值x做为SQL语句中的参数值
      • void setInt(int parameterIndex,int x) throws SQLException
        :将int值x做为SQL语句中的参数值
      • void setObject(int parameterIndex,Object x) throws SQLException
        :将object对象x做为SQL语句中的参数值
      • void setString(int parameterIndex,String x) throws SQLException
        : 将String值x做为SQL语句的参数值
      • void setTimestamp(int parameterIndex,Timestamp x) throws SQLException
        : 将java.sql.Timestamp值x做为SQL语句中的参数值
    • int executeUpdate() throws SQLException
      :
      executeUpdate()
      方法返回的
      int
      值表示受影响的行数。如果返回值为 0,则可能表示没有符合条件的记录被修改;执行
      INSERT

      UPDATE

      DELETE
      这些DML语句时同理

ResultSet接口

是用于接收查询的结果,是结果集合,当你执行一个SELECT语句时DBMS会返回一个包含查询结果的数据表,ResultSet接收用于表现这个数据表的对象

  • 作用:
    表示查询后的返回值
  • 主要方法
    • Boolean next() throws SQLException
      :移动游标到结果集的下一行,并返回一个boolen值,结尾返回false
    • getXXX(String columnLabel)
      :获取指定列名的值,XXX表示Java数据类返回XXX类型,如getString(),
      columnLabel
      表示列名
    • getXXX(int columnIndex)
      :获取指定列索引的值,列索引从 1 开始

SQL注入

所谓SQL注入,是值通过把恶意SQL语句插入到Web表单提交或页面请求的查询字符串,最终达到欺骗服务器的结果

SQL注入实例

  • 对于一个简单的登入功能,关键函数如下:
 static boolean noProtectLogin(String username, String password, Statement statement) throws SQLException {
        //username="abc";
        //password = "or '1'='1'";
        String sql = "SELECT *FROM  user WHERE username= '" + username + "'AND password=+''";
        ResultSet resultSet = statement.executeQuery(sql);
        return resultSet.next();
    }
  • 方法中的username于password没有进行任何处理,直接接受前端传入的数据,这样拼接的SQL语句会发送注入漏洞
  • 若把password参数修改成
    "or '1'='1'"
    ,username为任意值,那么这条语句结果为
    SELECT *FROM user WHERE username= 'abc' AND password= 'or '1'='1''
    显然这条语句的一直是true,这样可以把user表中的所有用户信息查询到,就可以成功实现无密码登入

SQL预编译

也称之为SQL预处理是一种,可以提高sql语句安全性和性能的技术

  • 预编译SQL语句允许在运行之前定义SQL语句结构,同时使用占位符(通常是问号
    ?
    )来动态表示数据部分

  • 在Java中可以使用
    PreparedStatement
    来创建和执行预编译的SQL语句,刚刚的登入操作使用
    PreparedStatement
    操作的代码如下

    static boolean noProtectLogin(String username, String password, Connection connection) throws SQLException {
    //      username="abc";
    //      password = "or '1'='1'";
            String sql = "SELECT * FROM  user WHERE username= ? AND password = ? ";
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.setString(1, username);
            statement.setString(2, password);
            ResultSet resultSet = statement.executeQuery();
            return resultSet.next();
        }
    

    • 这样我们在进行恶意的SQL注入,如把password定义成
      "or '1'='1'"
      执行结果直接返回false

事务

什么是事务? 官方的说法:事务是访问数据库的一个操作序列,数据库应用系统通过执行业务集合要完成对数据库的存取,简单来说:
事务是执行工作操作中最小的不可再分的工作单位
,通常一个业务对应一个事务,多个操作同时进行要么同时成功,要么同时失败,这就是事务

事务的理解

事务的特性

  • 原子性: 即不可分割,事务要么全部被执行,要么全部不执行
    .若所有的事务都提交成功,那么数据库操作被提交,数据库状态发生变化,
    若有一个子事务失败,那么其余事务的数据库操作都会回滚
    ,即数据库状态回到事务执行之前,保持状态不变
  • 一致性:事务的执行使得数据库从一种正确状态转换为另一个正确状态
  • 隔离性:在事务正确提交之前,不允许把事务对该数据的改变提交给其他事务,即在正确提交之前,其可能的结果不应该用于给其他事务
  • 持久性:即事务正确提交之后,其结果会永远保存在数据库之中,即事务提交之后有了其他故障,事务的处理结果也会得到保存

事务的通俗例子

  • 假设张三要给李四转账,要完成这个操作,要执行两个事务,一个是:扣除张三的账户余额;另一个是:李四的账余额增加
    ,这两个事务是不可分割的

事务的作用

  • 主要作用:保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不会破坏后台的数据完整性,拿ATM机举例子,若ATM在操作过程中突然出现故障,此时事务必须确保故障前对账号的操作不生效,确保用户于银行的利益不受损

JDBC中对事物进行管理

在JDBC中,Connection接口中定义了几个对事务操作的方法,我们一一讲解

  • void setAutoCommit*(*boolean autoCommit*)* throws SQLException
    :在默认情况下JDBC连接处于自动提交模式,这意味着每个SQL语句执行后都会立即提交,这明显不符合事务的特性,要我们进行显性关闭,即将
    autoCommit
    设置为false
  • void commit()
    :若所有操作都执行成功调用该方法提交事务
  • void rollback()
    :若事务中任何操作失败或出现异常错误则需调用
    rollback()
    回滚事务

银行存/取款举例

拿刚刚的张三和李四的例子说明

try {
            String sql1 = "UPDATE bank SET balance = balance - ? WHERE b_id=?";
            String sql2 = "UPDATE bank SET balance = balance + ? WHERE b_id=?";

            stmt1 = conn.prepareStatement(sql1);
            stmt2 = conn.prepareStatement(sql2);
            //事务1执行

            stmt1.setDouble(1, 500);
            stmt1.setString(2, "01");
            int r1 = stmt1.executeUpdate();
            //事务2执行

            stmt2.setDouble(1, 500);
            stmt2.setString(2, "02");
            int r2 = stmt2.executeUpdate();

            if (r1 == 1 && r2 == 1) {
                System.out.println("业务执行成功");
                conn.rollback();
            } else {
                System.out.println("业务执行失败");
                conn.commit();
            }
        } catch (SQLException e) {
            e.printStackTrace();
            //有异常回滚事务
            conn.rollback();
        } finally {
            Objects.requireNonNull(stmt1).close();
            Objects.requireNonNull(stmt2).close();
            conn.close();
        }

连接池

连接池是创建和管理数据库连接的技术,这些连接随时准备被任何需要它的线程使用

连接池的原理

  • 连接池的基本思想是在系统初始化时,将数据库连接
    作为对象存储在运行内存中
    ,
    当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象.使用完毕后,用户也并非将连接之间关闭,而是将连接放回连接池中,提供给下一次请求访问使用
    ,而连接池的建立,断开都由连接池自身来管理.同时,还可以
    通过设置连接池的参数来控制连接池中的初始连接数,连接的上下限数以及每个连接的最大使用次数,最大空闲时间等等
  • 连接池参数作用
    • 最小连接数:是连接池一直保持于数据库连接的数量
      ,因此若应用程序对数据库连接的使用量不大还设置较大的最小连接数,会造成大量的连接资源的浪费
    • 最大连接数:是连接池能申请的最大连接数,若数据库连接请求超过最大连接数,后续的数据库连接请求将被加入到等待队列中
    • 若min连接于max连接相差很大时,那么最先连接请求将会获利,之后超过min连接的连接请求等价于新建一个数据库连接,但这些大于min连接的数据库连接在使用之后不会马上被释放,将被放入连接池中等待重复利用

C3P0

C3P0是一个开放源代码的JDBC连接池,包括了jdbc3和jdbc2扩展规范说明的Connection和Statement池的DataSources对象

C3P0的配置

导入方法于导入JDBC-Mysql jar包类似这里不再赘述

  • 接着要配置c3p0-config.xml文件(这里选择xml配置)→直接配置到src文件夹下,否则会报配置错误

  • c3p0-config.xml文件的一般模板(可直接拷贝使用):

    <c3p0-config>
        <default-config>
            <!-- 数据库驱动名 -->
            <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
    
            <!-- 数据库的url -->
            <property name="jdbcUrl">jdbc:mysql://localhost:3306/demo</property>
    
            <!--用户名。Default: null -->
            <property name="user">root</property>
    
            <!--密码。Default: null -->
            <property name="password">root</property>
    
            <!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
            <property name="initialPoolSize">3</property>
    
            <!--连接池中保留的最大连接数。Default: 15 -->
            <property name="maxPoolSize">5</property>
            <!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
    		
            <property name="acquireIncrement">3</property>
            <!--最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
            <property name="maxIdleTime">60</property>
    
            <!--当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出 SQLException,如设为0则无限期等待。单位毫秒。Default:  0 -->
            <property name="checkoutTimeout">0</property>
        </default-config>
    </c3p0-config>
    

C3P0的使用方法

C3P0
中只有一个类,显然这个类是最主要的,其包含了C3P0所有对数据库连接的操作,接下来我们开始讲解

ComboPooledDataSource类

  • 主要方法


    • Connection getConnection()
      :从连接池中获取一个数据库连接,若当前没有空闲连接,则新建连接,直到达到最大连接数.其返回一个
      Connection
      对象

    除了使用xml文件配置还可以在代码中直接使用
    ComboPooledDataSource
    类中的方法配置,但一般使用xml文件,避免冗余


    • void setXXX()
      :设置C3P0中的各类属性,XXX表示属性,例如
      setUser(),setMinPoolSize(int min)
      等等
  • 使用连接池测试更新语句

    public static void main(String[] args) throws SQLException {
            //建立连接池,获取连接
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            Connection conn = dataSource.getConnection();
            //用连接池测试更新语句
            String sql = "SELECT * FROM  user WHERE username= ? AND password = ? ";
            PreparedStatement stmt = conn.prepareStatement(sql);
    
            stmt.setInt(1, 11111);
            stmt.setString(2, "11111111");
            ResultSet resultSet = stmt.executeQuery();
            if (resultSet.next()) {
                int username = resultSet.getInt("username");
                String password = resultSet.getString("password");
                System.out.println("username:" + username + "password:" + password);
            }
            stmt.close();
            conn.close();
        }
    

Druid(最好用的Java连接池)

Druid是目前最好数据库连接池,在功能.性能.扩展性方面都吊打其他连接池,包括
DBCP,C3P0,BoneCP,Proxool
等等

Druid配置

  • Druid执行要一个jar包大家可以去官网:
    https://repo1.maven.org/maven2/com/alibaba/druid/下载所需的jar包,在导入即可

  • Druid的参数列表


    属性(Parameter) 默认值(Default) 描述(Description)
    username **** 连接数据库的用户名
    password **** 连接数据库的密码
    jdbcUrl **** 同C3P0中的jdbcUrl属性
    driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
    initialSize 0 *
    初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 参见DBCP中的initialSize属性
    maxActive 8 最大连接池数量
    (Maximum number of Connections a pool will maintain at any given time.
    maxIdle 8 已经不再使用,配置了也没效果
    minIdle **** 最小连接池数量
    maxWait **** 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
    poolPreparedState- ments false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。
    maxOpenPrepared- Statements -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。      在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
    testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
    testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
    testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
    validationQuery **** 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、 testWhileIdle都不会其作用。在mysql中通常为select 'x',在oracle中通常为select 1 from dual
    timeBetweenEviction-RunsMillis **** 1) Destroy线程会检测连接的间隔时间 2) testWhileIdle的判断依据
    minEvictableIdle- TimeMillis **** Destory线程中如果检测到当前连接的最后活跃时间和当前时间的差值大于minEvictableIdleTimeMillis,则关闭当前连接。
    removeAbandoned **** 对于建立时间超过removeAbandonedTimeout的连接强制关闭
    removeAbandoned-Timeout **** 指定连接建立多长时间就需要被强制关闭
    logAbandoned false 指定发生removeabandoned的时候,是否记录当前线程的堆栈信息到日志中
    filters **** 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 1)监控统计用的filter:stat 2)日志用的filter:log4j  3)防御sql注入的filter:wall

    • 红色属性为必要配置属性
    • 定义application.properties(名字可以顺便取但必须是properties),可以放置任意目录下


    • application.properties文件一般模板如下

    driverClassName=com.mysql.cj.jdbc.Driver
    url=jdbc:mysql://localhost:3306/test?userSSL=false&serverTimezone=Asia/Shanghai
    username=root
    password=123456
    initialSize=3
    maxActive=5
    maxWait=1000
    

Druid使用方法

Druid也是只要一个核心类
DruidDataSource
它实现了
javax.sql.DataSource
接口

  • 主要类与方法


    • DruidDataSource()构造方法
      :构造一个默认的 DruidDataSource实例这个实例主要用于显性配置Druid属性
      • setXXX():
        设置属性值,如
        setUrl(String url)
        ,
        setInitialSize(int initialSize)
        等等
    • DruidDataSourceFactory
      类:是一个工厂类,用于根据提供的配置信息创建
      DruidDataSource
      实例,这个类简化了从配置文件中加载配置信息并创建
      DruidDataSource
      的过程
      • createDataSource(Properties properties)
        :最常用的方法之一,其接受一个Properties对象作为参数,从该对象读取配置信息,并创建一个
        DruidDataSource
  • 将Druid封装为JdbcUtils类

    public class JdbcUtil {
        private static DataSource dataSource;
    
        static {
            //先建立配置,连接连接池
            try {
                InputStream inputStream = JdbcUtil.class.getResourceAsStream("resource/application.properties");
                Properties props = new Properties();
                props.load(inputStream);
                dataSource = DruidDataSourceFactory.createDataSource(props);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static Connection getConnection() {
            Connection conn = null;
            try {
                conn = dataSource.getConnection();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return conn;
        }
    
        public static void close(ResultSet rs, PreparedStatement stmt, Connection conn) {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (stmt != null) {
                    stmt.close();
                }
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 本文借鉴同平台许多作者如
    @少平的博客人生,@chy_18883701161,@滥好人

musl Linux 和 glibc 是两种不同的 C 标准库实现,它们在多个方面存在显著差异。

  1. 历史和使用情况


    • glibc 是较早且广泛使用的 C 标准库实现,具有较长的开发历史和广泛的社区支持。它被大多数 Linux 发行版采用,特别是在桌面和服务器环境中。
    • musl 是一个相对较新的实现,旨在提供更小、更快、更安全的 C 库。它被一些轻量级 Linux 发行版如 Alpine Linux 采用。
  2. 功能和兼容性


    • glibc 功能全面且复杂,支持多种扩展和功能,具有较高的稳定性和可靠性。
    • musl 虽然功能较少,但更严格地遵循 POSIX 标准,且代码量比 glibc 少得多,不需要额外的外部依赖库。musl 的二进制兼容性有限,但随着新版本的发布,兼容性在逐步提高。
  3. 性能和资源占用


    • musl 设计为轻量级,适用于嵌入式系统和资源受限的环境,能够创建小巧的静态可执行文件。
    • glibc 虽然功能强大,但在资源占用和性能方面可能不如 musl。
  4. 调试和开发支持


    • glibc 由于其功能更全面,通常在应用调试和开发初期更受推荐。
    • musl 在某些调试工具(如 gdb 和 ltrace)的支持上可能不如 glibc。
  5. 许可证和社区支持


    • musl 采用 MIT 许可证,比 glibc 的 LGPL 许可证更宽松,便于发布静态可执行文件。
    • glibc 有更大的社区支持和更广泛的文档资源。
  6. 特定领域的应用


    • musl 在嵌入式系统、容器化应用和轻量级发行版中表现出色。
    • glibc 在桌面和服务器环境中更为常见,支持更多的功能和扩展。


musl libc 和 glibc 在 .NET 应用程序中的兼容性问题主要体现在以下几个方面:

  1. musl libc 和 glibc 都提供了 C 标准库函数的实现,理论上应用程序应该能够互换使用。然而,实际中发现这两个库在标准 libc 函数使用的系统调用上存在差异。这意味着即使两个库都实现了相同的 C 标准库函数,它们在底层调用的操作系统功能可能不同,从而导致兼容性问题。
  2. 在运行时环境方面,glibc 和 musl 的处理方式也有所不同。例如,Java 的 jpackage 和其他启动器需要修复以确保在不同平台上正确使用适当的 JDK 动态库。这表明 .NET 应用程序在使用 musl libc 时可能会遇到类似的动态库解析问题。
  3. 如果 .NET 应用包含本机库,则 musl libc 可能不兼容。Alpine Linux 使用 musl libc,而某些应用程序如果依赖于 glibc 提供的本机库,可能会在 Alpine 系统上运行失败。这种情况下,开发者需要特别注意应用程序对本机库的依赖,并确保这些依赖在 musl libc 环境下可用。
  4. 尽管 musl libc 在性能和体积上有优势,但其功能和行为与 glibc 存在显著差异。例如,高版本的 glibc 可能引入了新的 API 或改变了现有 API 的行为,这可能导致在低版本系统上运行时出现错误。因此,在使用 musl libc 替代 glibc 时,开发者需要仔细测试和验证应用程序的行为一致性。

musl 和 glibc 在多个具体方面存在差异,这些差异可能导致 .NET 应用程序在两者环境下运行时出现兼容性问题。以下是主要的差异:

  1. 实现方式和功能


    • musl libc 是一个简单、轻量级的 C 标准库,设计目标是实现纯粹的 C 标准,没有任何额外的功能。相比之下,glibc 提供了更多的扩展功能,适用于多数 Linux 系统。
    • musl 支持静态链接、实时性和内存效率,而 glibc 则提供了更广泛的功能和兼容性。
  2. 性能


    • musl 的 malloc 系列函数和 memcpy 系列函数可能实现较慢,特别是在多线程环境中。
  3. 二进制兼容性


    • musl 和 glibc 的二进制兼容性非常有限。虽然一些 glibc 链接的共享库可以在 musl 下加载,但大多数 glibc 链接的应用程序如果直接替换为 musl 将会失败。
  4. 平台和操作系统支持


    • glibc 具有广泛的兼容性,支持许多架构和操作系统。相比之下,musl 对其他平台和操作系统的移植性较差。
  5. 类型定义和结构体


    • musl libc 中的类型定义很有特点,重要类型都定义为联合体,只负责分配内存,至于类型本身的语义,则由实现宏来重新定义。
  6. 本地库兼容性


    • 如果 .NET 应用程序包含本地库(即那些依赖于特定 libc 实现的库),那么 musl 和 glibc 的不同可能会导致兼容性问题。大多数 .NET 应用程序不包括本地库,因此在这种情况下不需要担心这个细节

musl libc 和 glibc 在 .NET 应用程序中的兼容性问题主要包括系统调用的差异、动态库解析的不同、本机库依赖性以及版本冲突和功能差异等方面。在 musl Linux 和 glibc Linux 环境下运行 .NET 应用程序时,需要注意以下几点:

  1. glibc 环境下的 .NET 运行


    • 在 glibc 环境下,.NET 应用程序可能会遇到 glibc 版本不兼容的问题。例如,在碰到的案例中,运行 .NET 自包含可执行文件时可能会出现 glibc 错误。解决方法包括确认和更新 glibc 库、使用 Docker 容器运行应用程序以及尝试其他 .NET 的发行版。
    • 在 Linux 上,glibc 是主要的 C 库,许多 Linux 发行版都使用它。因此,.NET 应用程序在这些发行版上通常可以正常运行,前提是 glibc 版本与 .NET 运行时兼容。
  2. musl 环境下的 .NET 运行


    • musl 是一个轻量级的 C 库,常用于基于 musl 的 Linux 发行版,如 Alpine Linux。在 musl 环境下,.NET 应用程序可能会遇到 musl 版本不匹配的问题。例如,在 Stack Overflow 的讨论中,用户尝试降级 .NET 版本以匹配 musl 库,但遇到了加载库的问题。
    • .NET Core 3.0 及更高版本支持 musl,因此可以在 musl 环境下运行 .NET 应用程序。然而,musl 与 glibc 在某些方面存在差异,可能会导致兼容性问题。
  3. 兼容性和版本问题


    • 在 musl 和 glibc 环境下运行 .NET 应用程序时,需要注意 libc 库的版本兼容性。例如,在 Alpine 3.12 中,musl-libc 的版本是 1.1.24,而 .NET 6 的二进制文件可能缺少某些符号,导致运行问题。
    • 在 Linux 上部署 .NET 程序时,可能会遇到 .NET 运行环境与操作系统之间的不兼容性。因此,选择合适的 .NET 版本和 libc 库版本非常重要。
  4. 最佳实践

  • 为了实现最佳兼容性,建议选择长期支持版本(LTS)的 .NET 版本。
  • 在 musl 环境下,可以尝试降级 .NET 版本以匹配 musl 库,或者使用 Docker 容器来隔离运行环境。
  • 在 glibc 环境下,确保 glibc 库的版本与 .NET 运行时兼容,必要时进行升级。

在使用 Docker 容器在 musl 或 glibc 环境下运行 .NET 应用程序时,以下是一些最佳实践:

  1. 选择合适的镜像基础层


    • 如果你的应用程序需要 glibc(GNU C Library),可以选择包含 glibc 的基础镜像。例如,可以使用
      alpine
      镜像,它提供了 glibc 兼容性层
      libc6-compat
    • 如果你的应用程序不需要 glibc,或者你希望减少镜像大小,可以选择基于 musl 的镜像,如
      alpine
      镜像 。
  2. 多阶段构建


    • 使用多阶段构建来优化镜像大小和构建过程。这样可以在一个阶段中安装所有依赖项和工具,在另一个阶段仅复制最终的可执行文件到镜像中 。
  3. 解决版本冲突


    • 在 Docker 容器中,GLIBC 版本冲突可能导致程序无法正常运行。可以通过升级 GLIBC 库来解决这一问题,并提升系统的兼容性 。
  4. 初始化 Docker 资产


    • 使用
      docker init
      命令创建必要的 Docker 资产,包括 Dockerfile 和其他相关配置文件。这将帮助你更好地管理容器化应用程序 。
  5. 容器化与微服务架构


    • 微服务架构支持水平扩展,允许根据需要独立地扩展每个服务。可以在容器化环境中部署,如 Docker 和 Kubernetes,以实现更高的弹性和资源利用率 。
  6. 跨平台开发与部署


    • 利用 .NET Core 的跨平台特性,确保应用程序在不同操作系统上都能高效、便捷地开发与部署

总结来说,在 musl Linux 和 glibc Linux 环境下运行 .NET 应用程序时,需要特别注意 libc 库的版本兼容性,并根据具体情况选择合适的 .NET 版本和运行环境。

绪论

本合集将详细讲述如何实现基于群只能遗传算法的五子棋AI,采用C++作为底层编程语言
本篇将简要讨论实现思路,并在后续的文中逐一展开

了解五子棋

五子棋规则


五子棋是一种经典的棋类游戏,规则简单却充满策略性。游戏在一个19×19的棋盘上进行(也可以使用13×13或15×15的棋盘)。游戏的目标是率先在棋盘上连成五个相同颜色的棋子(横向、纵向或斜向)。

基本规则:

  1. 棋子:游戏使用两种颜色的棋子,通常为黑白两色。
  2. 落子:玩家轮流在棋盘上放置自己的棋子。
  3. 胜利条件:第一个在直线上(横向、纵向或对角线)连成五个棋子的玩家获胜。

五子棋操作简单,规则易懂,但需要很高的策略和技巧才能赢得比赛。

人类玩家是如何下五子棋的?

以下是一些五子棋对决的思路:

控制中心区域

  • 中心位置的重要性:棋盘中心的控制对游戏至关重要。控制中心区域可以给你更多的机会去创建和阻止对方的五子连线。

创建威胁

  • 连线威胁:尽量让对方必须防守而不能专注于自己的进攻。
  • 双活三:如果形成两个三子连线,并且这两个连线不会被对方轻易阻挡,就能够在几步内取得胜利。

防守对方的连线

  • 观察对方的棋子布局:注意对方棋子的排列,尤其是对方试图形成的三子、四子连线。
  • 及时阻挡:如果对方有连续的三子或四子的排列,应该优先阻挡对方的连线。

预判对方策略

  • 猜测对方意图:了解对方的策略,预测对方的下一步棋,提前做出相应的防御或进攻。

AI应该如何模仿?

为了让AI棋手学会下五子棋,甚至超越人类玩家的水平,首先应当有以下步骤:

  1. 理解棋盘信息:将棋盘的状态转换为程序能够处理的格式。这通常包括将棋盘上每个位置的状态(如空白、黑子或白子)编码为特定的数据结构,以便程序可以进行分析和处理。

  2. 设定行为集合:定义AI可以执行的操作范围。在五子棋中,AI可以在棋盘上任意未被占据的位置落子。

  3. 设定决策模式:确定AI的决策方式。本例中,AI采用贪心策略,即在每一步中选择预期回报最高的行动。贪心策略通过评估每个可能的落子位置的即时收益,选择对当前局势最有利的行动。

理解棋盘信息

理论上来讲,能够给AI提供的信息越多,那么AI做出的决策质量就越高,对于棋盘信息可以以格子为单位,评估该格子对于己方、和敌方的价值。

举例来说,如果在此处落子,敌方可以构成五子连珠,那么对于地方而言这是非常高价值的格子,那么在己方回合,当务之急是在此处落子,阻止对方胜利,除非在其它位置落子己方可以胜出。

对此,我们可以对棋盘上每一个可行位置进行打分,评估其对于己方、敌方的价值。

如何定义该位置对己方的价值?


一枚棋子可以在四个方向上与其它棋子连成五子,即:水平、竖直、对角线、主对角线

description

可以采用如下方法判断在某一具体方向上的价值

  1. 在四个方向中选择某一方向
  2. 向正方向、负方向分别查找4格,如遇到空格或敌方棋子则提前停止
  3. 统计己方棋子个数,以及两端的被遮拦情况。

例如对于下图,在该处落子后,形成水平方向上的两子连珠,且一端有遮拦一端无遮拦

description

一共可能形成如下几种情形,我们可以依据经验公式评估其价值。

子数 1子 2子 3子 4子 5子
无遮拦 MAX
一端遮拦 - -
两端遮拦 - - - -

'-' 表明该位置在该方向上具有的价值较低,不予考虑。然而,如果其他方向上的情况更有利,那么该位置的价值仍然可能非常高。。

接下来,我们可以讨论棋子在多个方向上的价值,一般来说,仅需要考虑最高价值的两个方向。

这是因为两个活三(无遮拦的三子连珠)足以致胜,三个活三并没有明显优势。

价值 最优方向 次优方向
Lv1 MAX ?
Lv1 ?
Lv2
Lv3
Lv3 -
Lv4
Lv4 -
Lv4 - -

“?” 指代任意情况,例如(MAX-?)对应了(MAX-MAX)、(MAX-①)、(MAX-②)、(MAX-③)、(MAX-'-')

该定义方式将行为的优先级分为了四个等级

  • Lv1:下子直接取胜,或在一回合内取胜。
  • Lv2:下在大概率在若干回合内取胜。
  • Lv3:能够迫使对方一直防御。
  • Lv4:收益较低。

如何综合攻防?


若要综合攻防,必须将具体位置对敌方的价值考虑进去。倘若某一位置对敌方来说是高价值的,那我们在此处落子可以破坏敌方阵型,削弱敌方价值,同样我们可以给出如下价值表

综合价值排序 己方价值 敌方价值 对应的奖励数值
1 Lv1 ? \(2^{20}\)
2 ? Lv1 \(2^{16}\)
3 Lv2 ? \(2^{12}\)
4 Lv2 \(2^{8}\)
5 Lv3 \(2^{4}\)
6 Lv4 \(2^{0}\)

“?” 指代任意情况,例如(Lv1-?)对应了(Lv1-Lv1)、(Lv1-Lv2)、(Lv1-Lv3)、(Lv1-Lv4)
在进行判断时,应当从上往下逐一判断。

这里给出的奖励数值仅供参考。

总结


在本小节中,我们精心构建了一种全新的综合评估方法,旨在全面衡量棋盘上的每个格子对于己方和敌方的战略价值。通过设计一系列精细的量化指标,我们赋予了AI/计算机深入解读棋盘格局的能力,使其能够准确判断每个格子的具体价值。这一方法为AI/计算机制定决策提供了坚实的数据支撑。

行为集规定与决策制定

为了使AI做出高效的决策,我们首先需要定义一套合适且简洁的行为集合。这意味着AI在做出选择时,不必每次都逐一考虑棋盘上的所有位置。在此基础上,我们需要开发一种策略,帮助AI从众多可能的决策中筛选出最为恰当的一个。通过这种方式,AI能够在复杂的环境中迅速而准确地做出最佳决策。

ROI 感兴趣区域


倘若上一轮你在棋盘中心落子,那么下一轮你不应当在棋盘的最角落落子。

一般来说,在落子时,只有与已有棋子(无论是己方还是敌方)邻接的位子才具有价值,首先我们定义邻接。

考虑棋盘上只有一子的情形,规划出与其具有高价值“联动”的区域如下:

description

可以给出更具体的定义:

倘若一个格子位于另一格子的水平、竖直、主对角线或副对角线方向上,并且两格子距离小于等于2,那么称这两个格子为
邻接
关系。进一步的,称距离为1为强邻接,距离为2为弱邻接。

进一步的,我们定义感兴趣区域如下:

满足以下要求之一的空格子为感兴趣区域:

  1. 该格子是棋盘
    正中心
  2. 该格子与至少一个
    己方
    棋子所处格子存在
    邻接
    关系。
  3. 该格子与至少一个
    敌方
    棋子所处格子存在
    强邻接
    关系。

下图给出了己方落子ROI区域的示例,其中红色为己方棋子,蓝色为对方棋子,灰色表示感兴趣区域。
description

决策进行


在进行决策前,我们可以评估感兴趣区域中所有格子的价值,假定ROI中格子的个数是
\(N\)
,格子的价值分别是
\(x_0, x_1, ..., x_{N-1}\)
,我们可以采用下述两种方法选择决策

硬最大值 hardmax

选择奖励最大的决策,即

\[h(\mathbf{x}) = \arg\max_{i} x_i
\]

软最大值 hardmax
不同于硬最大值,软最大值以一定几率接受非最优解,其包含一个常量
\(K\)
,常量K越大表示对低价值决策的接受程度越大,当常量
\(K\to 0\)
时,软最大值退化为
硬最大值
;当常量
\(K\to +\infty\)
时,软最大值退化为
随机抽取

\[\text{softmax}(x_i) = \frac{e^{x_i/K}}{\sum_{j=1}^N e^{x_j/K}}
\]

结语

下一篇中我们将继续讨论如何训练AI。

在最后,给出一个流程图供大家参考,在后文中我将详细讨论。

description

先看一下效果吧:

我们直接通过改造一下原版的TreeView来实现上面这个效果

我们先创建一个普通的TreeView

代码很简单:

        <TreeView>
            <TreeViewItem Header="人事部"/>
            <TreeViewItem Header="技术部">
                <TreeViewItem Header="技术部-1"/>
                <TreeViewItem Header="技术部-1"/>
            </TreeViewItem>
            <TreeViewItem Header="财务部"/>
        </TreeView>

实现的效果如下:

如果把这个当成是项目的菜单栏,应该会被领导骂死,一个是不够灵活,数据是写死的;二是样式不好看,只有点文字部分才会展开。

创建一下模板

直接在设计器中右键我们的item,编辑副本,点击确定,我们会得到下面一段代码

里面有一个叫Bd的border,我们把这个border的背景色去掉,然后我们自己去创建两个新的border

<Border Background="Transparent" Margin="-200,0,-200,0" Grid.ColumnSpan="4"/>
<Border x:Name="bd1" Background="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"
Margin="-200,0,-200,0" Visibility="Hidden" Grid.ColumnSpan="4">
<Border.Effect>
<DropShadowEffect BlurRadius="5" ShadowDepth="2"/>
</Border.Effect>
</Border>
<ToggleButton x:Name="Expander" ClickMode="Press"IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource Mode=TemplatedParent}}"Style="{StaticResource ExpandCollapseToggleStyle}"/> <Border x:Name="Bd" Grid.Column="1" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true"> <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </Border>

上面红色部分是我们新增的两个border,原本的叫Bd的border,我们只保留紫色部分的属性.

原本的代码里面有两个关于Bd的trigger

我们取名为bd1的border,最开始的Visibility设置的是Hidden,我们替换一下关于Bd的trigger,让它变成当IsSelected是true的情况下,让bd1的Visibility变成Visible.

<Trigger Property="IsSelected" Value="true">
    <Setter Property="Visibility" TargetName="bd1" Value="Visible"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
<MultiTrigger>
    <MultiTrigger.Conditions>
        <Condition Property="IsSelected" Value="true"/>
        <Condition Property="IsSelectionActive" Value="false"/>
    </MultiTrigger.Conditions>
    <Setter Property="Visibility" TargetName="bd1" Value="Visible"/>
<Setter Property="Background" TargetName="bd1" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>

再运行一下,看一下效果

基本上已经算是成功一半了,但是这个时候,我们的菜单只有一个有效果,其他的还是原来的样式,那是因为我们只有一个TreeViewItem使用了我们写的效果

如果我们每一个TreeViewItem都复制一下这句Style="{DynamicResource TreeViewItemStyle1}" ,是不是显得很呆,而且这只是在我们的菜单很少的情况下,如果菜单很多,这个方法就不可行。

所以这里我们用一个TreeView的ItemContainerStyle来操作一下

        <Style x:Key="treeViewStyle1" TargetType="{x:Type TreeView}" BasedOn="{StaticResource {x:Type TreeView}}">
            <Setter Property="ItemContainerStyle" Value="{StaticResource TreeViewItemStyle1}"/>
        </Style>

我们创建一个类型是TreeView的style,把它的ItemContainerStyle设置成我们之前添加的那个style,然后我们把这个style放到我们的TreeView上

这个时候我们再运行就会发现首级菜单的样式都实现我们想要的效果了,但是子集菜单还是原来的样式

我们在代码里面添加下面一个方法

        private voidApplyItemContainerStyle(ItemsControl itemsControl)
{
foreach (var item initemsControl.Items)
{
var treeViewItem = item asTreeViewItem;if (treeViewItem != null)
{
treeViewItem.Style
=treeview1.ItemContainerStyle;
ApplyItemContainerStyle(treeViewItem);
}
}
}

然后我们在构造函数里面把我们的TreeView当做是参数传进去

这个方法就是把所有的item和item的子项都设置成treeview的ItemContainerStyle;

我们再启动一下项目,就会发现效果是我们想要的效果了

到这里其实大部分效果都实现了,基本上也可以向领导交差了;

但是还缺少一个数据可拓展性和一个图标的功能,我们先看一下数据可拓展性

在平时的项目里面,一般都会有很多个不同的项目,每个项目可能都有好多个菜单,有的项目还想隐藏某一些菜单,我们总不能所有项目都通过visible属性来设置吧

特别是报表功能可能会有几十个,所以我们需要用到一个东西叫数据模板:HierarchicalDataTemplate;

我们先创建一个类

    public classTreeViewModel
{
public string Header { get; set; }public ObservableCollection<TreeViewModel> Children { get; set; }
}

然后回到设计器里面,把我们的代码改成下面的代码

<TreeView Style="{DynamicResource treeViewStyle1}" x:Name="treeview1">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="{x:Type local:TreeViewModel}" ItemsSource="{Binding Children}">
            <StackPanel Height="40" Orientation="Horizontal">
                <TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

对比一下红色部分的绑定,和类的属性,就能知道这个数据模板怎么用了.

再到构造函数里面去添加数据

public ObservableCollection<TreeViewModel> MenuCollection { get; set; }publicMainWindow()
{
InitializeComponent();


MenuCollection
= new ObservableCollection<TreeViewModel>()
{
new TreeViewModel
{
Header = "人事部"
},
new TreeViewModel
{
Header = "技术部",
Children = new ObservableCollection<TreeViewModel>
{
new TreeViewModel { Header = "技术部-1"},
new TreeViewModel { Header = "技术部-2"},
}
},
new TreeViewModel
{
Header = "财务部",
},
};

treeview1.ItemsSource =
MenuCollection;
}

注意这两段标红的代码,我们用一个集合MenuCollection模拟一下我们从数据库或者其他地方查询出来的菜单集合,然后把它做为数据源给treeview就可以了

再运行一下项目,它就差不多实现我们想要的效果了,现在再去找领导交差,领导还会夸你做的不错,只是还差一个图标了,这个就是锦上添花的东西了.

我们百度搜索一下  阿里ICON,去到官网里面,创建一个自己的账号,然后搜索一些自己喜欢的图标

把自己喜欢的图标添加到自己的项目中去,这里的项目名很重要,我取的是  FatSheep

在到我的项目里面去把这个资源文件下载到自己的项目中

下载下来的文件,我们把ttf后缀的文件添加到我们的项目里面去

把它作为资源引入到代码里面

<FontFamily x:Key="FatSheep">pack:application:,,,/自己项目的名字;component/Resources/iconfont.ttf#FatSheep</FontFamily>

记得修改一下自己的项目名字,我取的是TreeViewDemo,改成自己的项目名就好了,最后的结尾,是FatSheep,记得改成自己的ICON项目名称

接着我们在TreeViewModel里面添加一个Icon属性

    public classTreeViewModel
{
public string Header { get; set; }public string Icon { get; set; }public ObservableCollection<TreeViewModel> Children { get; set; }
}

然后我们在数据源里面添加一下数据

MenuCollection = new ObservableCollection<TreeViewModel>()
{
newTreeViewModel
{
Header
= "人事部",
Icon
= "\ue71c"},newTreeViewModel
{
Header
= "技术部",
Icon
= "\ue71c",
Children
= new ObservableCollection<TreeViewModel>{new TreeViewModel { Header = "技术部-1", Icon="\ue71c"},new TreeViewModel { Header = "技术部-2" , Icon="\ue71c"},
}
},
newTreeViewModel
{
Header
= "财务部",
Icon
= "\ue71c"},
};

设计器里面添加一下显示部分的代码

<TreeView Style="{StaticResource treeViewStyle1}" x:Name="treeView1" BorderThickness="0,0,1,0" Grid.Column="1">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="{x:Type local:TreeViewModel}" ItemsSource="{Binding Children}">
            <StackPanel Height="40" Orientation="Horizontal">
                <TextBlock Text="{Binding Icon}" VerticalAlignment="Center" FontFamily="{StaticResource FatSheep}" Margin="0,0,5,0"/>
                <TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

再启动项目,功能就完成了

这个笑脸是怎么来的了

那是因为我自己的项目里面添加了一个笑脸

我们复制一下这个代码,   &#xe71c;  我们把它改成 \ue71c,这是一个转义字符,就这样我们就能添加如何自己喜欢的图标了。

项目github地址:
bearhanQ/WPFFramework: Share some experience (github.com)

QQ技术交流群:332035933;

欢迎进群讨论问题,不论是winform,还是wpf,还是.net core的,还有很多萌妹.

本文使用的是dev-c++,如果涉及到VC++中不一样的操作,也会适当进行区分。

项目一:创建DLL

1、创建一个DLL类型的项目,当前命名为dlltest,并选择合适的路径进行保存。

2、在生成的预设置代码中,加入如下代码

//这是头文件dll.h
#ifndef _DLL_H_#define _DLL_H_

#if BUILDING_DLL
#define DLLIMPORT __declspec(dllexport)
#else
#define DLLIMPORT __declspec(dllimport)
#endif

classDLLIMPORT DllClass
{
public:
DllClass();
virtual ~DllClass();void HelloWorld(char*info);
};
extern "C"
{
DLLIMPORT int HW(int n);
}

DLLIMPORT int func(int n);
#endif
/*这是主体文件dllmain.cpp*/#include"dll.h"#include<windows.h>DllClass::DllClass()
{

}

DllClass::
~DllClass()
{

}
void DllClass::HelloWorld(char*info)
{
MessageBox(
0, info,"Hi",MB_ICONINFORMATION);
}

DLLIMPORT
int HW(int n)
{
return n;
}

DLLIMPORT int func(int n)
{
return
n;
}


BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved)
{
switch(fdwReason)
{
caseDLL_PROCESS_ATTACH:
{
break;
}
caseDLL_PROCESS_DETACH:
{
break;
}
caseDLL_THREAD_ATTACH:
{
break;
}
caseDLL_THREAD_DETACH:
{
break;
}
}
/*Return TRUE on success, FALSE on failure*/ returnTRUE;
}

在上面的代码中,我们加入了HW和func两个导出函数,以及一个DllClass(自动生成)导出类。

点击编译后,我们可以在项目文件夹中,看到dlltest.dll,这就是我们需要的目标动态链接库。libdlltest.a则是vc里需要用到的lib文件。

3、extern "C"说明

当前可以用记事本打到libdlltest.def文件,可以看到如下内容:

加了extern "C"的HW函数地址偏移量还是HW,没有加extern "C"的func函数,地址偏移量变成了_Z4funci。这个地址在动态调用导出函数的过程中会用到。

项目二:动态调用dll导出的函数

1、再创建一个C++项目,将项目一生成的dll文件放入项目文件夹中:

2、使用LoadLibrary和和GetProcAddress动态载入动态链接库,并调用导出的函数:

#include <iostream>#include<windows.h>
using namespacestd;intmain()
{
HMODULE hMod
=LoadLibrary("dlltest.dll");if(hMod==NULL)
{
cerr
<<"load lib error";return 1;
}

Func f
=(Func)GetProcAddress(hMod,"HW");
cout
<<f(200);

FreeLibrary(hMod);
return 0;
}

在GetProcAddress中,调用HW函数可以直接传入偏移量HW;

如果调用func函数,则要传入偏移量“_Z4funci”;因为func函数没有声明为extern "C"。

3、特别备注,当前这种方式无法使用LoadLibrary和GetProcAddress获取导出类。

因为GetProcAddress获取的是函数的地址偏移量,为了可以动态使用导出的类,必须使用将一个纯虚函数做为基类,将导出创建和销毁类的函数。具体做法如下:

//dll.h
#include <stdlib.h>#include<stdio.h>

classvirtualXXX
{
public:virtual void functionOne() = 0;virtual void functionTwo() = 0;
};
#if defined(_WINDOWS)#ifdef XXX_API#define XXX_API __declspec(dllexport) #else #define XXX_API __declspec(dllimport) #endif #else #define XXX_API #endif class XXX_API xxx : publicvirtualXXX
{
public:voidfunctionOne()
{
printf (
"One\n");
}
voidfunctionTwo()
{
printf (
"Two\n");
}
};
extern "C" XXX_API virtualXXX *create();extern "C" XXX_API void delete_object( virtualXXX *p );//dll.cpp virtualXXX *create()
{
return ( newxxx() );
}
void delete_object( virtualXXX *p )
{
if( p )
{
deletep;
p
=NULL;
}
}

动态调用:

#include <Windows.h>typedef virtualXXX*(fun_create)(void);
fun_create
* vc_create =NULL;intmain()
{
HINSTANCE dllHandle
=NULL;
dllHandle
= LoadLibrary( "Win32_Test_dll.dll");
vc_create
= ( fun_create* )GetProcAddress( dllHandle,"create");
virtualXXX
* xxxHandle =vc_create();


xxxHandle
->functionOne();
xxxHandle
->functionTwo();

delete_object(xxxHandle);
}

这个方法参考文章
C++动态库导出类,及使用
,博主未实际进行测试。

项目三:静态调用导出的类

静态调用dll,在VC++需要头文件、dll和对应的lib文件(即项目一中生成的 libdlltest.a)。然后再使用#pragma comment(lib,"lib文件路径")对编译器进行配置lib路径,之后再进行调用。详细过程可以参考
《c++生成DLL并调用》

本文着重调论Dev-C++下的静态调用。对于MinGW64静态调用dll,只需要dll文件和相关的头文件,项目结构如下:

其中main.cpp中调用类的代码如下:

#include <iostream>#include<windows.h>#include"dll.h" 
using namespacestd;intmain()
{
DllClass c;
char str[]="hello";
c.HelloWorld(str);
return 0;
}

可以看到,无需在代码中进行任何设置。因为只有在链接的过程c++才会去找DllClass这个类的真实地址。

在编译成exe时,有如下两种方法:

方法一、可以使用命令行进行编译:

通过cmd进入main.cpp所在文件夹路径,运行:
g++ -o main.exe main.cpp -I . -L . -ldlltest

即可编译生成可执行文件exe。编译参数说明如下:

-I搜索头文件的目录
-I .在当前文件夹下搜索头文件
-L搜索动态库的目录
-L .在当前文件夹下搜索动态库

方法二、将参数加入编译选项中:

如果觉得用命令行编译太麻烦,可以将-I -L和-l加入Dev-C++的编译器选项中。

这样点击“编译运行”就可以正确找到对应的dll进行编译链接,正确生成exe文件。

本文关于Dev-C++创建并调用动态链接库dll到这里就结束了,欢迎大家指正:)