草庐IT

Java实现Excel导入导出

梅西库里RNG 2023-04-21 原文

1、Excel导入

总结一下目标,就是要将excel中的数据行、逐一提取,最后得到一个list,这个list的每个元素就是excel的每个数据行的实例,之后的操作就是常规的java逻辑处理了。

可以把这个过程分为三步:

1)接收数据
后台使用 MultipartFile 接收文件数据(这里复习一下,springmvc接收参数的几种方式)。

前端如果是前后端分离项目我们不用管,
如果是jsp项目可以用文件标签<input type="file" name="" value=""/>传参。

2)提取数据
提取数据的底层是用IO流实现的,我们这里使用封装好的工具类。excel工具类有很多、很多,我一般是使用适配性最好的,不然一会儿springboot项目、一会儿spring项目等等,还得换不同的工具类;效率什么的不是首要考虑项。
使用工具类后,我们就得到了一个List<List<String>>:Excel的每个数据行组成一个List<String>,多个数据行就组成了List<List<String>>

这里有一个小坑,说明一下;有时候你导入的数据,如果是数字比如32位的卡号等等,用工具类提取出来成了科学计数法、或者后面加了小数点;这就说明你用的这个工具类没有将数字类型数据进行处理,你需要在工具类中找到数字类型,添加toText()方法。
当然你也可以使用我后面提供的工具类,这个问题已经作了处理。

3)将list的元素处理成实例对象,方便后续处理
在作转换的时候,还可以加一些校验、限制,比如限制excel导入总行数不得超过多少、限制某列参数不能重复等等。

下面提供一下我用的excel导入工具类,springboot或者spring项目都可以用

public class ImportExeclUtil {

    private static int totalRows = 0;// 总行数

    private static int totalCells = 0;// 总列数

    private static String errorInfo;// 错误信息

    /** 无参构造方法 */
    public ImportExeclUtil()
    {
    }

    public static int getTotalRows()
    {
        return totalRows;
    }

    public static int getTotalCells()
    {
        return totalCells;
    }

    public static String getErrorInfo()
    {
        return errorInfo;
    }

    /**
     *
     * 根据流读取Excel文件
     *
     *
     * @param inputStream
     * @param isExcel2003
     * @return
     * @see [类、类#方法、类#成员]
     */
    public List<List<String>> read(InputStream inputStream, boolean isExcel2003)
            throws IOException
    {

        List<List<String>> dataLst = null;

        /** 根据版本选择创建Workbook的方式 */
        Workbook wb = null;

        if (isExcel2003)
        {
            wb = new HSSFWorkbook(inputStream);
        }
        else
        {
            wb = new XSSFWorkbook(inputStream);
        }
        dataLst = readDate(wb);

        return dataLst;
    }

    /**
     *
     * 读取数据
     *
     * @param wb
     * @return
     * @see [类、类#方法、类#成员]
     */
    private List<List<String>> readDate(Workbook wb)
    {

        List<List<String>> dataLst = new ArrayList<List<String>>();

        /** 得到第一个shell */
        Sheet sheet = wb.getSheetAt(0);

        /** 得到Excel的行数 */
        totalRows = sheet.getPhysicalNumberOfRows();

        /** 得到Excel的列数 */
        if (totalRows >= 1 && sheet.getRow(0) != null)
        {
            totalCells = sheet.getRow(0).getPhysicalNumberOfCells();
        }

        /** 循环Excel的行 */
        for (int r = 1; r < totalRows; r++)
        {
            Row row = sheet.getRow(r);
            if (row == null)
            {
                continue;
            }

            List<String> rowLst = new ArrayList<String>();

            /** 循环Excel的列 */
            for (int c = 0; c < getTotalCells(); c++)
            {

                Cell cell = row.getCell(c);
                String cellValue = "";

                if (null != cell)
                {
                    // 以下是判断数据的类型
                    switch (cell.getCellTypeEnum())
                    {
                        case NUMERIC: // 数字
                            //如果是日期的话
                            if(cell != null && HSSFDateUtil.isCellDateFormatted(cell)){
                                Date d = cell.getDateCellValue();
                                DateFormat formater = new SimpleDateFormat("yyyy/MM/dd");
                                String da = formater.format(d);
                                cellValue = da;
                                break;
                            }
                            cellValue = NumberToTextConverter.toText(cell.getNumericCellValue());
                            break;

                        case STRING: // 字符串
                            cellValue = cell.getStringCellValue();
                            break;

                        case BOOLEAN: // Boolean
                            cellValue = cell.getBooleanCellValue() + "";
                            break;

                        case FORMULA: // 公式
                            cellValue = cell.getCellFormula() + "";
                            break;

                        case BLANK: // 空值
                            cellValue = "";
                            break;

                        case ERROR: // 故障
                            cellValue = "非法字符";
                            break;

                        default:
                            cellValue = "未知类型";
                            break;
                    }
                }

                rowLst.add(cellValue);
            }

            /** 保存第r行的第c列 */
            dataLst.add(rowLst);
        }

        return dataLst;
    }

    /**
     *
     * 根据Excel表格中的数据判断类型得到值
     *
     * @param cell
     * @return
     * @see [类、类#方法、类#成员]
     */
    /*private static String getCellValue(Cell cell)
    {
        String cellValue = "";

        if (null != cell)
        {
            // 以下是判断数据的类型
            switch (cell.getCellType())
            {
                case HSSFCell.CELL_TYPE_NUMERIC: // 数字
                    ;: // 数字
                    if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell))
                    {
                        Date theDate = cell.getDateCellValue();
                        SimpleDateFormat dff = new SimpleDateFormat("yyyy-MM-dd");
                        cellValue = dff.format(theDate);
                    }
                    else
                    {
                        DecimalFormat df = new DecimalFormat("0");
                        cellValue = df.format(cell.getNumericCellValue());
                    }
                    break;
                case HSSFCell.CELL_TYPE_STRING: // 字符串
                    cellValue = cell.getStringCellValue();
                    break;

                case HSSFCell.CELL_TYPE_BOOLEAN: // Boolean
                    cellValue = cell.getBooleanCellValue() + "";
                    break;

                case HSSFCell.CELL_TYPE_FORMULA: // 公式
                    cellValue = cell.getCellFormula() + "";
                    break;

                case HSSFCell.CELL_TYPE_BLANK: // 空值
                    cellValue = "";
                    break;

                case HSSFCell.CELL_TYPE_ERROR: // 故障
                    cellValue = "非法字符";
                    break;

                default:
                    cellValue = "未知类型";
                    break;
            }

        }
        return cellValue;
    }*/

    /**
     *
     * 根据实体成员变量的类型得到成员变量的值
     *
     * @param realValue
     * @param fields
     * @param f
     * @param cellValue
     * @return
     * @see [类、类#方法、类#成员]
     */
    private static Object getEntityMemberValue(Object realValue, Field[] fields, int f, String cellValue)
    {
        String type = fields[f].getType().getName();
        switch (type)
        {
            case "char":
            case "java.lang.Character":
            case "java.lang.String":
                realValue = cellValue;
                break;
            case "java.util.Date":
                realValue = StringUtils.isBlank(cellValue) ? null : DateUtil.strToDate(cellValue, DateUtil.YYYY_MM_DD);
                break;
            case "java.lang.Integer":
                realValue = StringUtils.isBlank(cellValue) ? null : Integer.valueOf(cellValue);
                break;
            case "int":
            case "float":
            case "double":
            case "java.lang.Double":
            case "java.lang.Float":
            case "java.lang.Long":
            case "java.lang.Short":
            case "java.math.BigDecimal":
                realValue = StringUtils.isBlank(cellValue) ? null : new BigDecimal(cellValue);
                break;
            default:
                break;
        }
        return realValue;
    }

    /**
     *
     * 根据路径或文件名选择Excel版本
     *
     *
     * @param filePathOrName
     * @param in
     * @return
     * @throws IOException
     * @see [类、类#方法、类#成员]
     */
    public static Workbook chooseWorkbook(String filePathOrName, InputStream in)
            throws IOException
    {
        /** 根据版本选择创建Workbook的方式 */
        Workbook wb = null;
        boolean isExcel2003 = ExcelVersionUtil.isExcel2003(filePathOrName);

        if (isExcel2003)
        {
            wb = new HSSFWorkbook(in);
        }
        else
        {
            wb = new XSSFWorkbook(in);
        }

        return wb;
    }

    static class ExcelVersionUtil
    {

        /**
         *
         * 是否是2003的excel,返回true是2003
         *
         *
         * @param filePath
         * @return
         * @see [类、类#方法、类#成员]
         */
        public static boolean isExcel2003(String filePath)
        {
            return filePath.matches("^.+\\.(?i)(xls)$");

        }

        /**
         *
         * 是否是2007的excel,返回true是2007
         *
         *
         * @param filePath
         * @return
         * @see [类、类#方法、类#成员]
         */
        public static boolean isExcel2007(String filePath)
        {
            return filePath.matches("^.+\\.(?i)(xlsx)$");

        }

    }

    public static class DateUtil
    {

        // ======================日期格式化常量=====================//

        public static final String YYYY_MM_DDHHMMSS = "yyyy-MM-dd HH:mm:ss";

        public static final String YYYY_MM_DD = "yyyy-MM-dd";

        public static final String YYYY_MM = "yyyy-MM";

        public static final String YYYY = "yyyy";

        public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";

        public static final String YYYYMMDD = "yyyyMMdd";

        public static final String YYYYMM = "yyyyMM";

        public static final String YYYYMMDDHHMMSS_1 = "yyyy/MM/dd HH:mm:ss";

        public static final String YYYY_MM_DD_1 = "yyyy/MM/dd";

        public static final String YYYY_MM_1 = "yyyy/MM";

        /**
         *
         * 自定义取值,Date类型转为String类型
         *
         * @param date 日期
         * @param pattern 格式化常量
         * @return
         * @see [类、类#方法、类#成员]
         */
        public static String dateToStr(Date date, String pattern)
        {
            SimpleDateFormat format = null;

            if (null == date)
                return null;
            format = new SimpleDateFormat(pattern, Locale.getDefault());

            return format.format(date);
        }

        /**
         * 将字符串转换成Date类型的时间
         * <hr>
         *
         * @param s 日期类型的字符串<br>
         *            datePattern :YYYY_MM_DD<br>
         * @return java.util.Date
         */
        public static Date strToDate(String s, String pattern)
        {
            if (s == null)
            {
                return null;
            }
            Date date = null;
            SimpleDateFormat sdf = new SimpleDateFormat(pattern);
            try
            {
                date = sdf.parse(s);
            }
            catch (ParseException e)
            {
                e.printStackTrace();
            }
            return date;
        }
    }

}

再提供一个应用实例

	@ApiOperation(value = "以导入excel方式,上传要申请学分的用户")
    @GetMapping(value = "/uuApplyUserInfo")
    public AjaxResult uuApplyUserInfo(@RequestParam(value = "files",required = false) MultipartFile files) {
        try {
            //工具类
            ImportExeclUtil readExcelUtil = new ImportExeclUtil();
            List<List<String>> read = readExcelUtil.read(files.getInputStream(), true);

            if (CollectionUtils.isNotEmpty(read)){

                List<ApplyCreditUserDto> importList = read.stream().map(e -> {
                    ApplyCreditUserDto importDto = new ApplyCreditUserDto();
                    importDto.setUserName(e.get(0));
                    importDto.setCreditCardNo(e.get(1));
                    importDto.setCreditCardPwd(e.get(2));
                    return importDto;
                }).collect(Collectors.toList());

                if (CollectionUtils.isEmpty(importList)){
                    return AjaxResult.error("不能导入空文件");
                }

                //最多导入1W条
                final int maxInt = 10000;
                if (importList.size() > maxInt){
                    return AjaxResult.error("导入最多修改1W条");
                }

                List<String> orderIds = importList.stream()
                        .map(ApplyCreditUserDto::getUserName)
                        .distinct()
                        .collect(Collectors.toList());
                if (!Objects.equals(orderIds.size(),importList.size())){
                    return AjaxResult.error("导入信息中,有用户");
                }

                //调用业务层
                return applyCreditLogService.uuApplyUserInfo(importList);

            }else{
                return AjaxResult.error("不能导入空文件");
            }

        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.error("导入失败,更新数据库时报错!报错信息:" + e.toString());
        }
    }

2、将数据导出为Excel

这里先说明一下这里要做的是把从数据库查到的数据行,导出到excel;和下载一个静态文件(比如下载Excel静态文件模板),是不一样的需求。

1)引入依赖

这里用的是poi导出excel,版本如下

<!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
      <dependency>
          <groupId>org.apache.poi</groupId>
          <artifactId>poi</artifactId>
          <version>4.1.1</version>
      </dependency>

2)自定义注解

为了方便,提高复用效率,这里自定义2个注解;
第一个:@EnableExcel,用来开启Excel表格的导出,用在装导出数据的实体类上;

/**
 *  标记类是否开启Excel
 * @Author: Sunlong
 * @date: 2020/5/10 20:29
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableExcel {
}

第二个:@ExcelRow,用在装导出数据的实体类的属性上,用来映射字段与excel的对应关系;

/**
 *  excel 表格 列名注解
 *
 * @author sunlong
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelRow {

    /**
     *  Excel 对应列名
     * @return
     */
    String name();

    /**
     *  excel 列名备注
     * @return
     */
    String note() default "";
}

3)代码逻辑—提取导出工具类

a、通过反射获取自定义注解EnableExcel 判断是否开启Excel导出

b、通过反射获取自定义注解ExcelRow 获取列对应的属性

c、把属性对应的列下标取出来,属性名做为key,下标做为value放到map中

d、遍历要导出的数据集合,通过属性描述器PropertyDescriptor获取对应属性下标及属性值并设置到cell单元格中

还是为了方便,已经提高复用效率,我们将上述代码提取成一个工具类,如下:

public class ExportExcelUtils {

    /**
     *  workbook
     * @param titleList
     * @return
     */
    public static HSSFWorkbook getWorkBook(List<String> titleList){
        //第一步,创建一个workbook,对应一个Excel文件
        HSSFWorkbook wb = new HSSFWorkbook();
        // 一个sheet
        HSSFSheet sheet = wb.createSheet("sheet1");

        HSSFRow rowTitle = sheet.createRow(0); // 第一行 标题

        // 设置标题
        for (int i = 0; i < titleList.size(); i++) {
            HSSFCell cell = rowTitle.createCell(i);
            cell.setCellValue(titleList.get(i));
        }
        //合并单元格CellRangeAddress构造参数依次表示起始行,截至行,起始列, 截至列
        /*sheet.addMergedRegion(new CellRangeAddress(0,0,0,4));
        sheet.addMergedRegion(new CellRangeAddress(titleList.size()-1,titleList.size()-1,titleList.size()-1,titleList.size()+1));*/
        return wb;
    }


    public static <T> HSSFWorkbook getWorkBook(List<String> titleList , List<T> dataList) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
        if (CollectionUtils.isNotEmpty(dataList)) {
            T t1 = dataList.get(0);
            Class<?> t1Class = t1.getClass();
            EnableExcel enableExcel = t1Class.getAnnotation(EnableExcel.class);
            if (enableExcel == null) {
                throw new IllegalArgumentException("EnableExcel 注解没有在实体类启用");
            }
            Field[] fields = t1Class.getDeclaredFields();
            if (fields != null && fields.length > 0) {
                Map<String , Integer> titleMap = new HashMap<>(titleList.size()); // 存放属性名称对应的下标

                int fieldExcelSize = 0; // 类中ExcelRow 注解的数量
                for (Field field : fields) {
                    field.setAccessible(true);
                    String fieldName = field.getName();
                    ExcelRow excelRow = field.getAnnotation(ExcelRow.class);
                    if (excelRow != null) {
                        String name = excelRow.name();
                        if (StringUtils.isEmpty(name)) {
                            throw new IllegalArgumentException("ExcelRow 注解name属性不能为空");
                        }

                        int index = titleList.indexOf(name.trim());
                        if (index != -1) {
                            fieldExcelSize++;
                            titleMap.put(fieldName , index);
                        }
                    }
                }

                if (!(titleList.size() == fieldExcelSize)) {
                    throw new IllegalArgumentException("ExcelRow 注解name属性对应的列数不对");
                }

                HSSFWorkbook workBook = getWorkBook(titleList);
                HSSFSheet sheet = workBook.getSheetAt(0);

                for (T t : dataList) {
                    int lastRowNum = sheet.getLastRowNum();
                    HSSFRow row = sheet.createRow(lastRowNum + 1);
                    BeanInfo beanInfo = Introspector.getBeanInfo(t.getClass());
                    PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
                    for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                        String fieldName = propertyDescriptor.getName();
                        if (titleMap.containsKey(fieldName)) {
                            Method readMethod = propertyDescriptor.getReadMethod();
                            if (readMethod != null) {
                                Class<?> returnType = readMethod.getReturnType();
                                String simpleName = returnType.getSimpleName();

                                Object invoke = readMethod.invoke(t);
                                String value = "";
                                // 可以根据不同的类型返回不同的数据
                                if ("date".equalsIgnoreCase(simpleName)) {
                                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                                    if (invoke != null) {
                                        value = simpleDateFormat.format(invoke);
                                    }
                                }
                                if (invoke != null && "".equals(value)) {
                                    value = invoke.toString();
                                }
                                row.createCell(titleMap.get(fieldName)).setCellValue(value);
                            }
                        }
                    }
                }
                return workBook;
            }
        }
        return null;
    }
}

4)应用实例

创建一个用来装导出数据的类(为了不与项目其他功能冲突,我一般都是新建一个专门做导出的实体类)

@EnableExcel
@Data
public class UserEntity {

    @ExcelRow(name = "name")
    private String username;

    @ExcelRow(name = "pass")
    private String password;

    @ExcelRow(name = "date")
    private Date createDate;

}

模拟导出功能:

public class Test {
    public static void main(String[] args) throws IllegalAccessException, IntrospectionException, InvocationTargetException, IOException {

        List<String> titleList = new ArrayList<>();
        titleList.add("name");
        titleList.add("pass");
        titleList.add("date");

        List<UserEntity> userEntities = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            UserEntity userEntity1 = new UserEntity();
            userEntity1.setUsername("username"+i);
            userEntity1.setPassword("password"+i);
            userEntity1.setCreateDate(new Date());
            userEntities.add(userEntity1);
        }

        HSSFWorkbook workBook = ExportExcelUtils.getWorkBook(titleList, userEntities);
        if (workBook != null) {
            File file = new File("D:\\test_export.xlsx");
            workBook.write(file);
        }
        
    }
}

有关Java实现Excel导入导出的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  4. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  5. ruby - 检查是否通过 require 执行或导入了 Ruby 程序 - 2

    如何检查Ruby文件是否是通过“require”或“load”导入的,而不是简单地从命令行执行的?例如:foo.rb的内容:puts"Hello"bar.rb的内容require'foo'输出:$./foo.rbHello$./bar.rbHello基本上,我想调用bar.rb以不执行puts调用。 最佳答案 将foo.rb改为:if__FILE__==$0puts"Hello"end检查__FILE__-当前ruby​​文件的名称-与$0-正在运行的脚本的名称。 关于ruby-检查是否

  6. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  7. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  8. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  9. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  10. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

随机推荐