jjzjj

Cesium项目功能实现记录

知心宝贝 2024-05-24 原文


目录


我将对我在最近与数字孪生项目的对接过程中所实现的一些功能进行总结。这些功能主要涉及到地理信息系统方面的Cesium详细功能设计。具体来说,我在这个项目中实现了一些功能,包括对接不同地图平台和引擎,实现地图数据可视化和交互式控制,以及在Cesium中添加和操作各种地图元素等。

1 切换二维地图

    viewer = new Cesium.Viewer('cesiumContainer', {
        sceneMode: Cesium.SceneMode.SCENE2D,//切换2D
    })

2 删除默认图层

viewer.imageryLayers.removeAll()

3 隐藏版权信息

viewer._cesiumWidget._creditContainer.style.display = "none"

4 加载cesiumlab切片影像出现栅格阴影

原因:未设置切片影像加载区域

// 矩形区域
bestRect= Cesium.Rectangle.fromRadians(
  2.086396706185367,
  0.6039399681488924,
  2.086691132076495,
  0.6042171535375767)
const xinWeiImagery = new Cesium.UrlTemplateImageryProvider({
    url: '',
    rectangle:bestRect
})

5 解决相机控制问题

// 如果为真,则允许用户旋转相机。如果为假,相机将锁定到当前标题。此标志仅适用于2D和3D。
scene.screenSpaceCameraController.enableRotate = false;
// 如果为true,则允许用户平移地图。如果为假,相机将保持锁定在当前位置。此标志仅适用于2D和Columbus视图模式。
scene.screenSpaceCameraController.enableTranslate = false;
// 如果为真,允许用户放大和缩小。如果为假,相机将锁定到距离椭圆体的当前距离
scene.screenSpaceCameraController.enableZoom = false;
// 如果为真,则允许用户倾斜相机。如果为假,相机将锁定到当前标题。这个标志只适用于3D和哥伦布视图。
scene.screenSpaceCameraController.enableTilt = false;

viewer.scene.screenSpaceCameraController.minimumZoomDistance = 80//相机的高度的最小值
viewer.scene.screenSpaceCameraController.maximumZoomDistance = 40000 //相机高度的最大值
// viewer.scene.screenSpaceCameraController._minimumZoomRate = 100 // 设置相机缩小时的速率
// viewer.scene.screenSpaceCameraController._maximumZoomRate = 12000 //设置相机放大时的速率

6 cesium中限制地图浏览范围

概述:鼠标只能在指定地点移动,出界则返回

// 控制鼠标左键移动问题
let setCameraMove = () => {

    viewer.scene.preRender.addEventListener(() => {
        let rectangle = viewer.camera.computeViewRectangle();
       
        //设置可浏览经纬度范围
         let maxRange = { west: 118.30112, north: 35.21500, east: 120.78832, south: 34.26431 };
        //地理坐标(弧度)转经纬度坐标
        // 弧度转为经纬度,west为左(西)侧边界的经度,以下类推
        
        let west = Cesium.Math.toDegrees(rectangle.west).toFixed(5)
        let south = Cesium.Math.toDegrees(rectangle.south).toFixed(5)
        let east = Cesium.Math.toDegrees(rectangle.east).toFixed(5)
        let north = Cesium.Math.toDegrees(rectangle.north).toFixed(5)



        // 119.13933 34.45136 120.02731 34.79189
        //  console.log(rectangle);
        // console.log(west,south,east,north);
        //如果视角超出设置范围则跳转视角
        if (west < maxRange.west || south < maxRange.south  || east > maxRange.east  || north > maxRange.north) {
            setCameraPos()
        }
    })
}

参考文章:cesium中限制地图浏览范围

7 鼠标移动显示经纬度

概述:悬浮框跟随屏幕坐标,将屏幕坐标转为经纬度展示

const showInfoDom = document.querySelector('.show-info')
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

// 鼠标移动显示经纬度
handler.setInputAction((target) => {

    // console.log(viewer.scene.camera.heading, viewer.scene.camera.pitch, viewer.scene.camera.roll);
    // 鼠标移动终点
    const position = target.endPosition
    // 椭球面
    const ellipsoid = viewer.scene.globe.ellipsoid
    const cartesian = viewer.camera.pickEllipsoid(position, ellipsoid)

    // 只选取地球表面
    if (cartesian && isShowMouseMoveInfo.value) {
        // 空间笛卡尔转换为弧度
        const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
        // 弧度转为经纬度
        const lon = Cesium.Math.toDegrees(cartographic.longitude).toFixed(5)
        const lat = Cesium.Math.toDegrees(cartographic.latitude).toFixed(5)
        const height = cartographic.height;
        // console.log(`经度为:${lon},纬度为:${lat},高度为:${height}`);
        showInfoDom.innerHTML = `经度为:${lon},<br>纬度为:${lat}`
        showInfoDom.style.display = 'block'
        showInfoDom.style.top = `${position.y}px`
        showInfoDom.style.left = `${position.x + 30}px`

    } else {
        showInfoDom.style.display = 'none'
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE)

8 禁用cesium选取实体操作

概述:当我们鼠标双击实体时,cesium默认相机定位并在右侧显示实体的名称

8.1 禁用操作

viewer = new Cesium.Viewer('cesiumContainer', {

    selectionIndicator: false,//双击选中实体
    sceneModePicker: false,
})

8.2 双击事件改写

handler.setInputAction(() => {
     viewer.trackedEntity = undefined
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)

8.3 信息隐藏(index.html页面)

<style>
  /* 禁止双击事件弹出框 */
  .cesium-viewer-infoBoxContainer{
    display: none;
    /* background-color: rgb(133, 207, 236); */
  }
</style>

9 自定义动画

概述:一般可以用cesium官方的时钟搞,这个我试了一下定时器的效果

// 添加小车模型
let car7
const staticCar = carStore.staticCar
const dynamicCar = carStore.dynamicCar
let loadCarTrail = () => {


    // 加载静态小车
    for (let i = 0; i < staticCar.length; i++){
        viewer.entities.add({
            name: staticCar[i].name,
            position: Cesium.Cartesian3.fromDegrees(staticCar[i].position.lon,staticCar[i].position.lat,staticCar[i].position.height),
            model: {
                uri: 'model/car/CesiumMilkTruck.glb'
            },
            label: {
                text: staticCar[i].name,
                font: "20px sans-serif",
                // showBackground: true,
                distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
                    0,
                    500
                ),
                eyeOffset: new Cesium.Cartesian3(0, 3.5, 0),
            }
        })
    }

    // 加载动态小车
    car7= viewer.entities.add({
        name: dynamicCar.name,
        position: Cesium.Cartesian3.fromDegrees(dynamicCar.position.lon, dynamicCar.position.lat, dynamicCar.position.height),
        model: {
            uri: 'model/car/CesiumMilkTruck.glb',
            scale:2
        },
        label:{
            text: '装载机',
            font: "15px sans-serif",
            // showBackground: true,
            distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
                0,
                500
            ),
            eyeOffset: new Cesium.Cartesian3(0, 6.6, 0),
        }
    })
}
// 车辆开始作业
let timer,i=0
let startDynamicCar = () => {

  // 规律:经度增加10,纬度减小2
  // 经度偏移量0.0005
  setdynamicCarCamera()
  if (!timer) {
    console.log('开始作业!!!');
    let car7Pos = car7.position._value
    let temp
    if (!timer) {
      timer = setInterval(() => {
        temp = Cesium.Cartesian3.fromDegrees(dynamicCar.trail[i].lon, dynamicCar.trail[i].lat,0)
        car7Pos.x = temp.x
        car7Pos.y = temp.y
        car7Pos.z = temp.z


        i++
        if (i >= dynamicCar.trail.length) {
          i = 0
        }
      }, 100);
    }
  }
}

// 车辆结束作业
let endDynamicCar = () => {
  console.log('结束作业!!!');
  clearInterval(timer)
  timer = null
}

10 小车轨迹切分

概述:比如给定任意三个坐标,将相邻的两个坐标中间的点进行切分,这样动画会流畅,下面的代码还没有写完,但思路差不多

// 轨迹切分
let sliceCarTrail = () => {

    const sliceArr = [
        { lon: 119.54520, lat: 34.61526, height: 0 },
        { lon: 119.54750, lat: 34.61496, height: 0 },
        { lon: 119.54980, lat: 34.61462, height: 0 }
    ]
    const startPos = { lon: 119.54520, lat: 34.61525, height: 0 }
    const endPos = { lon: 119.54990, lat: 34.61466, height: 0 }


    const lonNum = 0.00010
    const latNum = 0.00002
    
    
    const num=0.00005
    for (let i = 0; i < sliceArr.length - 1; i++){
        // 计算经纬度切片数量
        const k1 = Math.abs(Math.round((sliceArr[i + 1].lon - sliceArr[i].lon) / lonNum))
        const k2 = Math.abs(Math.round((sliceArr[i + 1].lat - sliceArr[i].lat) / latNum))
        const k = Math.max(k1, k2)

        for (let j = 0; j < k; j++){

            const middlePos = { lon: parseFloat((sliceArr[i].lon + j * lonNum).toFixed(5)), lat: parseFloat((sliceArr[i].lat + j * lonNum).toFixed(5)), height: 0 }
            
            dynamicCar.trail.push(middlePos)
        }
    }
    dynamicCar.trail.push(sliceArr[sliceArr.length - 1])
    console.log(dynamicCar.trail);
}

11 label跟随模型

概述:可以在添加实体模型同时添加label实体

viewer.entities.add({
      name: loaderCar[i].name,
      position: Cesium.Cartesian3.fromDegrees(loaderCar[i].position.lon, loaderCar[i].position.lat, loaderCar[i].position.height),
      model: {
          uri: 'model/car/CesiumMilkTruck.glb'
      },
      label: {
          text: new Cesium.CallbackProperty((result) => {
              let carInfo =`车辆名称:${loaderCar[i].name}\n车辆类型:${loaderCar[i].type}\n工作信息:${loaderCar[i].jobInfo}\n其他信息:${loaderCar[i].other}`

              return carInfo

          }, false),
          font: labelStore.font,
          showBackground: labelStore.showBackground,
          backgroundColor: labelStore.backgroundColor,
          outlineColor: labelStore.outlineColor,
          horizontalOrigin: labelStore.horizontalOrigin,
          verticalOrigin: labelStore.verticalOrigin,
          // pixelOffset: new Cesium.Cartesian2(0,0),
          disableDepthTestDistance: labelStore.disableDepthTestDistance,//被建筑物遮挡问题
          scaleByDistance: labelStore.scaleByDistance,
              distanceDisplayCondition: labelStore.distanceDisplayCondition
      }
  })

12 自定义label样式

概述:一般来说,label实体内置了很多属性,但是样式比较单一,如果想自定义样式可以构造组件

在Cesium中,Label实体的配置项包括:

text:标签中显示的文本内容。

font:标签的字体样式。

pixelOffset:标签相对于实体位置的偏移量。

showBackground:是否显示标签的背景。

backgroundColor:标签背景的颜色。

backgroundPadding:标签背景的内边距。

fillColor:标签文本的填充颜色。

outlineColor:标签文本的轮廓线颜色。

outlineWidth:标签文本的轮廓线宽度。

scale:标签的缩放比例。

verticalOrigin:标签垂直方向的定位方式。

horizontalOrigin:标签水平方向的定位方式。

translucencyByDistance:标签的透明度随距离变化的效果。

pixelOffsetScaleByDistance:标签的偏移量随距离变化的效果。

distanceDisplayCondition:标签的显示距离条件。

这些配置项可以用于创建和修改Cesium中的Label实体。

参考链接:cesium自定义label标签

13 轨迹输出坐标点

概述:需求按照一定的时间间隔逐步输出打印的点,这里有两种方法

  • 先把点实体全部加载到场景(设置为隐藏),通过定时器逐步显示
  • 添加点前延时1s添加,这里用的是第二种
// 开始绘制小车打点轨迹
const points = trailCar[0].points
let drawPointTimer=null
let drawPointTrail = async () => {


  // viewer.camera.
  viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(119.54876, 34.61479,300), // 设置位置,北京的坐标
    orientation: {
      heading: 0,
      pitch: Cesium.Math.toRadians(-90.0),
      roll: 0
    }
  })
  console.log('开始绘制轨迹!!!');
  isShowPointTrail.value=true
  for (let i = 0; i < points.length; i++) {
    await new Promise(resolve => setTimeout(resolve, 1000)); // 设置延迟,观察效果
    viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(points[i].position.lon, points[i].position.lat, points[i].position.height),
      point: {
        color: Cesium.Color.RED,
        outlineColor: Cesium.Color.PINK,
        outlineWidth: 5,
        pixelSize: 20,
        // heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,逐步添加有问题注释掉
        scaleByDistance: new Cesium.NearFarScalar(100, 1, 1000, 0.05)
      },
      label: {
        text: new Cesium.CallbackProperty((result) => {
          let carInfo = `${points[i].time}`

          return carInfo

        }, false),
        showBackground: true,
        font: "12px monospace",
        horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        // pixelOffset: new Cesium.Cartesian2(0,0),
        disableDepthTestDistance: Number.POSITIVE_INFINITY,//被建筑物遮挡问题
        scaleByDistance: new Cesium.NearFarScalar(100, 2, 500, 0.1)
      }
    });

  }

}

14 识别实体模型

概述:需要鼠标点击或移动经过实体的时候,识别出来实体并显示信息

14.1 识别3D Titles模型

// 定义一个点击事件
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
handler.setInputAction(function(movement) {
    const feature = viewer.scene.pick(movement.position);
    if (feature instanceof Cesium.Cesium3DTileFeature) {
        feature.color = Cesium.Color.RED; // 将拾取到的3D tiles颜色修改为红色
    }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

14.2 识别一般实体

// 鼠标左键弹出选择框
handler.setInputAction((target) => {
    const position= target.position
    // 返回拾取顶端的具有primitive属性的一个对象
    const feature = viewer.scene.pick(position)

    if (feature && feature.id._id === 'test001' && !isShowPointTrail.value) {
            ElMessageBox.confirm(
                '是否显示小车近十分钟轨迹?',
                {
                    confirmButtonText: '确认',
                    cancelButtonText: '取消',
                }
            )
            .then(() => {
            
                drawPointTrail()
                 ElMessage({
                    type: 'success',
                    message: '成功显示',
                })
            })
            .catch(() => {
                ElMessage({
                    type: 'info',
                    message: '取消显示',
                })
            })           
    } 
    
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK)

15 修改3DTitles高度

// 接下来解决模型移动缩放的问题
let changeModel = (tileset, height) => {
    // 显示3D Tiles包围盒
    // tileset.debugShowContentBoundingVolume = true

    const cartographic = Cesium.Cartographic.fromCartesian(
        tileset.boundingSphere.center
    );
    const surface = Cesium.Cartesian3.fromRadians(
        cartographic.longitude,
        cartographic.latitude,
        0.0
    );
    const offset = Cesium.Cartesian3.fromRadians(
        cartographic.longitude,
        cartographic.latitude,
        height
    );
    const translation = Cesium.Cartesian3.subtract(
        offset,
        surface,
        new Cesium.Cartesian3()
    );
    tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
}

16 解决影像拼接黑色锯齿

概述:在大疆智图导出的2D影像,进行多个拼接时,边缘部分出现明显的锯齿痕迹。但是后期通过抗锯齿化没有办法解决,这里可以利用加载影像的最大范围缩小。

let loadImageryLayer = () => {
    const slice_total = new Cesium.UrlTemplateImageryProvider({
        url: 'http://localhost:9003/image/wmts/21Q3Ey3F/{z}/{x}/{y}',
        rectangle: Cesium.Rectangle.fromDegrees(119.21356, 34.60827, 119.21464, 34.60928),

    })
    viewer.imageryLayers.addImageryProvider(slice_total)

    const slice_cehui = new Cesium.UrlTemplateImageryProvider({
        url: 'http://localhost:9003/image/wmts/mC7YGixZ/{z}/{x}/{y}',
        rectangle: Cesium.Rectangle.fromDegrees(119.21362, 34.60876, 119.21451, 34.6090),


    })
    viewer.imageryLayers.addImageryProvider(slice_cehui)

    const slice_wensi = new Cesium.UrlTemplateImageryProvider({
        url: 'http://localhost:9003/image/wmts/v46lxY3m/{z}/{x}/{y}',
        rectangle: Cesium.Rectangle.fromDegrees(119.21384, 34.60534, 119.21464, 34.60866),


    })
    viewer.imageryLayers.addImageryProvider(slice_wensi)


    const slice_cehui_right = new Cesium.UrlTemplateImageryProvider({
        url: 'http://localhost:9003/image/wmts/QvyCMwx1/{z}/{x}/{y}',
        // 设置纹理过滤选项
        rectangle: Cesium.Rectangle.fromDegrees(119.2144, 34.609, 119.21449, 34.60909)


    })
    viewer.imageryLayers.addImageryProvider(slice_cehui_right)
}

17 去除cesium默认功能

概述:一般来说,部署的时候cesium很多的功能需要去掉,这里我们可以直接选择CesiumWidget或者添加配置选项。

viewer = new Cesium.Viewer('cesiumContainer', {
      geocoder: false, // 隐藏查找位置
      homeButton: false, // 隐藏返回视角到初始位置
      sceneModePicker: false, // 隐藏视角模式的选择
      baseLayerPicker: false, // 隐藏图层选择器
      navigationHelpButton: false, // 隐藏帮助
      animation: false, // 隐藏动画速度控制器
      timeline: false, // 隐藏时间轴
      fullscreenButton: false, // 隐藏全屏按钮
      shouldAnimate: true
})

18 vite全局整合cesium

// 创建vue项目
npm create vite

//导入cesium和依赖插件

地址:https://github.com/nshen/vite-plugin-cesium

19 相机定位问题

viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(119.54876, 34.61479, 300), 
    orientation: {
        heading: 0,
        pitch: Cesium.Math.toRadians(-90.0),
        roll: 0
    }
})

有关Cesium项目功能实现记录的更多相关文章

  1. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  2. ruby-on-rails - 项目升级后 Pow 不会更改 ruby​​ 版本 - 2

    我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby​​版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby​​版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘

  3. ruby - Sinatra:运行 rspec 测试时记录噪音 - 2

    Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/

  4. ruby-on-rails - 新 Rails 项目 : 'bundle install' can't install rails in gemfile - 2

    我已经像这样安装了一个新的Rails项目:$railsnewsite它执行并到达:bundleinstall但是当它似乎尝试安装依赖项时我得到了这个错误Gem::Ext::BuildError:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcheckingforlibkern/OSAtomic.h...yescreatingMakefilemake"DESTDIR="cleanmake"DESTDIR="

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

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

  6. ruby-on-rails - Rails 5 Active Record 记录无效错误 - 2

    我有两个Rails模型,即Invoice和Invoice_details。一个Invoice_details属于Invoice,一个Invoice有多个Invoice_details。我无法使用accepts_nested_attributes_forinInvoice通过Invoice模型保存Invoice_details。我收到以下错误:(0.2ms)BEGIN(0.2ms)ROLLBACKCompleted422UnprocessableEntityin25ms(ActiveRecord:4.0ms)ActiveRecord::RecordInvalid(Validationfa

  7. Ruby 从大范围中获取第 n 个项目 - 2

    假设我有这个范围:("aaaaa".."zzzzz")如何在不事先/每次生成整个项目的情况下从范围中获取第N个项目? 最佳答案 一种快速简便的方法:("aaaaa".."zzzzz").first(42).last#==>"aaabp"如果出于某种原因你不得不一遍又一遍地这样做,或者如果你需要避免为前N个元素构建中间数组,你可以这样写:moduleEnumerabledefskip(n)returnto_enum:skip,nunlessblock_given?each_with_indexdo|item,index|yieldit

  8. ruby-on-rails - Cucumber 是否只是 rspec 的包装器以帮助将测试组织成功能? - 2

    只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您

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

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

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

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

随机推荐